From c5a4843c941ba61e810076fd625b9c92123fddcd Mon Sep 17 00:00:00 2001 From: Andrea Pinto Date: Sun, 18 Oct 2026 02:04:23 +0200 Subject: [PATCH 1/6] Refresh API client and endpoint coverage --- internal/api/client.gen.go | 2200 ++++++++++++++++++++++++++++----- scripts/endpoint-coverage.txt | 4 +- 2 files changed, 1882 insertions(+), 322 deletions(-) diff --git a/internal/api/client.gen.go b/internal/api/client.gen.go index 6fc850f..5d2d5c1 100644 --- a/internal/api/client.gen.go +++ b/internal/api/client.gen.go @@ -121,9 +121,9 @@ const ( // Defines values for BrandCacheMetadataStatus. const ( - Hit BrandCacheMetadataStatus = "hit" - Miss BrandCacheMetadataStatus = "miss" - Zdr BrandCacheMetadataStatus = "zdr" + BrandCacheMetadataStatusHit BrandCacheMetadataStatus = "hit" + BrandCacheMetadataStatusMiss BrandCacheMetadataStatus = "miss" + BrandCacheMetadataStatusZdr BrandCacheMetadataStatus = "zdr" ) // Defines values for BrandColorSource. @@ -1084,6 +1084,12 @@ const ( StyleguideModeLight StyleguideMode = "light" ) +// Defines values for StyleguideCacheMetadataStatus. +const ( + StyleguideCacheMetadataStatusHit StyleguideCacheMetadataStatus = "hit" + StyleguideCacheMetadataStatusMiss StyleguideCacheMetadataStatus = "miss" +) + // Defines values for StyleguideResponseFinalDOMState. const ( Loaded StyleguideResponseFinalDOMState = "loaded" @@ -2167,16 +2173,18 @@ type CreditCardDictOutput struct { // DataScrapeRequest defines model for DataScrapeRequest. type DataScrapeRequest struct { - Formats ScrapeFormats `json:"formats"` - MarkdownParams *MarkdownParams `json:"markdownParams,omitempty"` - MaxAgeMs *int `json:"maxAgeMs,omitempty"` - ParseParams *MapsParseParams `json:"parseParams,omitempty"` - ProductParams *ProductParams `json:"productParams,omitempty"` - SharedParams *SharedParams `json:"sharedParams,omitempty"` - Tags *[]string `json:"tags,omitempty"` - TimeoutOpts *StyleguideTimeout `json:"timeoutOpts,omitempty"` - Url string `json:"url"` - Zdr *DataScrapeRequestZdr `json:"zdr,omitempty"` + Formats ScrapeFormats `json:"formats"` + MarkdownParams *MarkdownParams `json:"markdownParams,omitempty"` + + // MaxAgeMs Maximum cached result age in milliseconds. Omit for server policy; zero forces fresh extraction. + MaxAgeMs *int `json:"maxAgeMs,omitempty"` + ParseParams *MapsParseParams `json:"parseParams,omitempty"` + ProductParams *ProductParams `json:"productParams,omitempty"` + SharedParams *SharedParams `json:"sharedParams,omitempty"` + Tags *[]string `json:"tags,omitempty"` + TimeoutOpts *StyleguideTimeout `json:"timeoutOpts,omitempty"` + Url string `json:"url"` + Zdr *DataScrapeRequestZdr `json:"zdr,omitempty"` } // DataScrapeRequestZdr defines model for DataScrapeRequest.Zdr. @@ -4852,10 +4860,13 @@ type StyleguideMode string // StyleguideCacheMetadata defines model for StyleguideCacheMetadata. type StyleguideCacheMetadata struct { - AgeMs *int `json:"age_ms,omitempty"` - Status *string `json:"status,omitempty"` + AgeMs *int `json:"age_ms,omitempty"` + Status *StyleguideCacheMetadataStatus `json:"status,omitempty"` } +// StyleguideCacheMetadataStatus defines model for StyleguideCacheMetadata.Status. +type StyleguideCacheMetadataStatus string + // StyleguideResponse defines model for StyleguideResponse. type StyleguideResponse struct { CacheMetadata StyleguideCacheMetadata `json:"cache_metadata"` @@ -5284,20 +5295,74 @@ type ListAuditEventsParams struct { // ListDataApisParams defines parameters for ListDataApis. type ListDataApisParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // BrandRetrieveParams defines parameters for BrandRetrieve. type BrandRetrieveParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` +} + +// InvalidateParams defines parameters for Invalidate. +type InvalidateParams struct { + Capability *string `form:"capability,omitempty" json:"capability,omitempty"` + + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` +} + +// GetPolicyParams defines parameters for GetPolicy. +type GetPolicyParams struct { + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // MapsSearchParams defines parameters for MapsSearch. type MapsSearchParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // ListMonitorsParams defines parameters for ListMonitors. @@ -5312,14 +5377,31 @@ type ListMonitorsParams struct { ChangeDetectionType *string `form:"change_detection_type,omitempty" json:"change_detection_type,omitempty"` Tags *string `form:"tags,omitempty" json:"tags,omitempty"` Tag *string `form:"tag,omitempty" json:"tag,omitempty"` - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // CreateParams defines parameters for Create. type CreateParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // AccountChangesParams defines parameters for AccountChanges. @@ -5332,117 +5414,258 @@ type AccountChangesParams struct { Tag *string `form:"tag,omitempty" json:"tag,omitempty"` Since *string `form:"since,omitempty" json:"since,omitempty"` Until *string `form:"until,omitempty" json:"until,omitempty"` - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // CreditUsageParams defines parameters for CreditUsage. type CreditUsageParams struct { - Since *string `form:"since,omitempty" json:"since,omitempty"` - Until *string `form:"until,omitempty" json:"until,omitempty"` - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + Since *string `form:"since,omitempty" json:"since,omitempty"` + Until *string `form:"until,omitempty" json:"until,omitempty"` + + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // LimitsParams defines parameters for Limits. type LimitsParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // AccountRunsParams defines parameters for AccountRuns. type AccountRunsParams struct { - Limit *int `form:"limit,omitempty" json:"limit,omitempty"` - Cursor *string `form:"cursor,omitempty" json:"cursor,omitempty"` - Status *string `form:"status,omitempty" json:"status,omitempty"` - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + Limit *int `form:"limit,omitempty" json:"limit,omitempty"` + Cursor *string `form:"cursor,omitempty" json:"cursor,omitempty"` + Status *string `form:"status,omitempty" json:"status,omitempty"` + + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // DeleteParams defines parameters for Delete. type DeleteParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // RetrieveParams defines parameters for Retrieve. type RetrieveParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // UpdateParams defines parameters for Update. type UpdateParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // ChangesParams defines parameters for Changes. type ChangesParams struct { - Limit *int `form:"limit,omitempty" json:"limit,omitempty"` - Cursor *string `form:"cursor,omitempty" json:"cursor,omitempty"` - Tag *string `form:"tag,omitempty" json:"tag,omitempty"` - Since *string `form:"since,omitempty" json:"since,omitempty"` - Until *string `form:"until,omitempty" json:"until,omitempty"` - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + Limit *int `form:"limit,omitempty" json:"limit,omitempty"` + Cursor *string `form:"cursor,omitempty" json:"cursor,omitempty"` + Tag *string `form:"tag,omitempty" json:"tag,omitempty"` + Since *string `form:"since,omitempty" json:"since,omitempty"` + Until *string `form:"until,omitempty" json:"until,omitempty"` + + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // RetrieveChangeParams defines parameters for RetrieveChange. type RetrieveChangeParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // RunNowParams defines parameters for RunNow. type RunNowParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // RunsParams defines parameters for Runs. type RunsParams struct { - Limit *int `form:"limit,omitempty" json:"limit,omitempty"` - Cursor *string `form:"cursor,omitempty" json:"cursor,omitempty"` - Status *string `form:"status,omitempty" json:"status,omitempty"` - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + Limit *int `form:"limit,omitempty" json:"limit,omitempty"` + Cursor *string `form:"cursor,omitempty" json:"cursor,omitempty"` + Status *string `form:"status,omitempty" json:"status,omitempty"` + + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // RetrieveRunParams defines parameters for RetrieveRun. type RetrieveRunParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // RotateSecretParams defines parameters for RotateSecret. type RotateSecretParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // NewsSearchParams defines parameters for NewsSearch. type NewsSearchParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // PeopleEnrichParams defines parameters for PeopleEnrich. type PeopleEnrichParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // ExtractProductEndpointParams defines parameters for ExtractProductEndpoint. type ExtractProductEndpointParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } -// ScrapeSpecializedDataParams defines parameters for ScrapeSpecializedData. -type ScrapeSpecializedDataParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` +// FetchSpecializedDataParams defines parameters for FetchSpecializedData. +type FetchSpecializedDataParams struct { + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // StyleguideEndpointParams defines parameters for StyleguideEndpoint. @@ -5455,41 +5678,92 @@ type StyleguideEndpointParams struct { TimeoutOptsBehavior *string `form:"timeoutOpts[behavior],omitempty" json:"timeoutOpts[behavior],omitempty"` Zdr *string `form:"zdr,omitempty" json:"zdr,omitempty"` Tags *string `form:"tags,omitempty" json:"tags,omitempty"` - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // DeliveriesParams defines parameters for Deliveries. type DeliveriesParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // RetrieveDeliveryParams defines parameters for RetrieveDelivery. type RetrieveDeliveryParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // AttemptsParams defines parameters for Attempts. type AttemptsParams struct { - Limit *int `form:"limit,omitempty" json:"limit,omitempty"` - Cursor *string `form:"cursor,omitempty" json:"cursor,omitempty"` - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + Limit *int `form:"limit,omitempty" json:"limit,omitempty"` + Cursor *string `form:"cursor,omitempty" json:"cursor,omitempty"` + + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // RetryParams defines parameters for Retry. type RetryParams struct { - IdempotencyKey *string `json:"Idempotency-Key,omitempty"` - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + IdempotencyKey *string `json:"Idempotency-Key,omitempty"` + + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // YoutubeEndpointParams defines parameters for YoutubeEndpoint. type YoutubeEndpointParams struct { - XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` - XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` + // XNotteCacheMode Extraction cache: default, refresh, or bypass. + XNotteCacheMode *string `json:"X-Notte-Cache-Mode,omitempty"` + + // XNotteCacheMaxAgeMs Maximum cached result age in milliseconds; zero forces refresh. + XNotteCacheMaxAgeMs *string `json:"X-Notte-Cache-Max-Age-Ms,omitempty"` + + // XNotteCacheTtlSeconds Shorten the server storage TTL; zero bypasses caching. + XNotteCacheTtlSeconds *string `json:"X-Notte-Cache-Ttl-Seconds,omitempty"` + XNotteRequestOrigin *string `json:"x-notte-request-origin,omitempty"` + XNotteSdkVersion *string `json:"x-notte-sdk-version,omitempty"` } // ListFunctionsParams defines parameters for ListFunctions. @@ -6225,8 +6499,8 @@ type PeopleEnrichJSONRequestBody = PeopleEnrichRequest // ExtractProductEndpointJSONRequestBody defines body for ExtractProductEndpoint for application/json ContentType. type ExtractProductEndpointJSONRequestBody = ProductExtractRequest -// ScrapeSpecializedDataJSONRequestBody defines body for ScrapeSpecializedData for application/json ContentType. -type ScrapeSpecializedDataJSONRequestBody = DataScrapeRequest +// FetchSpecializedDataJSONRequestBody defines body for FetchSpecializedData for application/json ContentType. +type FetchSpecializedDataJSONRequestBody = DataScrapeRequest // DeliveriesJSONRequestBody defines body for Deliveries for application/json ContentType. type DeliveriesJSONRequestBody = DeliveryList @@ -14213,6 +14487,12 @@ type ClientInterface interface { BrandRetrieve(ctx context.Context, params *BrandRetrieveParams, body BrandRetrieveJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // Invalidate request + Invalidate(ctx context.Context, params *InvalidateParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetPolicy request + GetPolicy(ctx context.Context, params *GetPolicyParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // MapsSearchWithBody request with any body MapsSearchWithBody(ctx context.Context, params *MapsSearchParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -14282,10 +14562,10 @@ type ClientInterface interface { ExtractProductEndpoint(ctx context.Context, params *ExtractProductEndpointParams, body ExtractProductEndpointJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - // ScrapeSpecializedDataWithBody request with any body - ScrapeSpecializedDataWithBody(ctx context.Context, params *ScrapeSpecializedDataParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + // FetchSpecializedDataWithBody request with any body + FetchSpecializedDataWithBody(ctx context.Context, params *FetchSpecializedDataParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - ScrapeSpecializedData(ctx context.Context, params *ScrapeSpecializedDataParams, body ScrapeSpecializedDataJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + FetchSpecializedData(ctx context.Context, params *FetchSpecializedDataParams, body FetchSpecializedDataJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) // StyleguideEndpoint request StyleguideEndpoint(ctx context.Context, params *StyleguideEndpointParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -14787,6 +15067,30 @@ func (c *Client) BrandRetrieve(ctx context.Context, params *BrandRetrieveParams, return c.Client.Do(req) } +func (c *Client) Invalidate(ctx context.Context, params *InvalidateParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewInvalidateRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetPolicy(ctx context.Context, params *GetPolicyParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetPolicyRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) MapsSearchWithBody(ctx context.Context, params *MapsSearchParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewMapsSearchRequestWithBody(c.Server, params, contentType, body) if err != nil { @@ -15087,8 +15391,8 @@ func (c *Client) ExtractProductEndpoint(ctx context.Context, params *ExtractProd return c.Client.Do(req) } -func (c *Client) ScrapeSpecializedDataWithBody(ctx context.Context, params *ScrapeSpecializedDataParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewScrapeSpecializedDataRequestWithBody(c.Server, params, contentType, body) +func (c *Client) FetchSpecializedDataWithBody(ctx context.Context, params *FetchSpecializedDataParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewFetchSpecializedDataRequestWithBody(c.Server, params, contentType, body) if err != nil { return nil, err } @@ -15099,8 +15403,8 @@ func (c *Client) ScrapeSpecializedDataWithBody(ctx context.Context, params *Scra return c.Client.Do(req) } -func (c *Client) ScrapeSpecializedData(ctx context.Context, params *ScrapeSpecializedDataParams, body ScrapeSpecializedDataJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewScrapeSpecializedDataRequest(c.Server, params, body) +func (c *Client) FetchSpecializedData(ctx context.Context, params *FetchSpecializedDataParams, body FetchSpecializedDataJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewFetchSpecializedDataRequest(c.Server, params, body) if err != nil { return nil, err } @@ -17371,26 +17675,59 @@ func NewListDataApisRequest(server string, params *ListDataApisParams) (*http.Re if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -17437,46 +17774,68 @@ func NewBrandRetrieveRequestWithBody(server string, params *BrandRetrieveParams, if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) } - } + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string - return req, nil -} + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) + } -// NewMapsSearchRequest calls the generic MapsSearch builder with application/json body -func NewMapsSearchRequest(server string, params *MapsSearchParams, body MapsSearchJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err } - bodyReader = bytes.NewReader(buf) - return NewMapsSearchRequestWithBody(server, params, "application/json", bodyReader) + + return req, nil } -// NewMapsSearchRequestWithBody generates requests for MapsSearch with any type of body -func NewMapsSearchRequestWithBody(server string, params *MapsSearchParams, contentType string, body io.Reader) (*http.Request, error) { +// NewInvalidateRequest generates requests for Invalidate +func NewInvalidateRequest(server string, params *InvalidateParams) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -17484,7 +17843,7 @@ func NewMapsSearchRequestWithBody(server string, params *MapsSearchParams, conte return nil, err } - operationPath := fmt.Sprintf("/data/maps/search") + operationPath := fmt.Sprintf("/data/cache") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -17494,35 +17853,273 @@ func NewMapsSearchRequestWithBody(server string, params *MapsSearchParams, conte return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } + if params != nil { + queryValues := queryURL.Query() - req.Header.Add("Content-Type", contentType) + if params.Capability != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "capability", runtime.ParamLocationQuery, *params.Capability); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } if params != nil { + if params.XNotteCacheMode != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Mode", headerParam0) + } + + if params.XNotteCacheMaxAgeMs != nil { + var headerParam1 string + + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) + } + + } + + return req, nil +} + +// NewGetPolicyRequest generates requests for GetPolicy +func NewGetPolicyRequest(server string, params *GetPolicyParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/data/cache") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + if params != nil { + + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) + } + + if params.XNotteCacheMaxAgeMs != nil { + var headerParam1 string + + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) } if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) + } + + } + + return req, nil +} + +// NewMapsSearchRequest calls the generic MapsSearch builder with application/json body +func NewMapsSearchRequest(server string, params *MapsSearchParams, body MapsSearchJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewMapsSearchRequestWithBody(server, params, "application/json", bodyReader) +} + +// NewMapsSearchRequestWithBody generates requests for MapsSearch with any type of body +func NewMapsSearchRequestWithBody(server string, params *MapsSearchParams, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/data/maps/search") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + if params != nil { + + if params.XNotteCacheMode != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Mode", headerParam0) + } + + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -17722,26 +18319,59 @@ func NewListMonitorsRequest(server string, params *ListMonitorsParams) (*http.Re if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -17788,26 +18418,59 @@ func NewCreateRequestWithBody(server string, params *CreateParams, contentType s if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -17975,26 +18638,59 @@ func NewAccountChangesRequest(server string, params *AccountChangesParams) (*htt if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18066,26 +18762,59 @@ func NewCreditUsageRequest(server string, params *CreditUsageParams) (*http.Requ if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18112,33 +18841,66 @@ func NewLimitsRequest(server string, params *LimitsParams) (*http.Request, error return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) - if err != nil { - return nil, err - } + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + if params != nil { + + if params.XNotteCacheMode != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Mode", headerParam0) + } + + if params.XNotteCacheMaxAgeMs != nil { + var headerParam1 string + + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } - if params != nil { + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } if params.XNotteRequestOrigin != nil { - var headerParam0 string + var headerParam3 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("x-notte-request-origin", headerParam3) } if params.XNotteSdkVersion != nil { - var headerParam1 string + var headerParam4 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18226,26 +18988,59 @@ func NewAccountRunsRequest(server string, params *AccountRunsParams) (*http.Requ if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18286,26 +19081,59 @@ func NewDeleteRequest(server string, monitorId string, params *DeleteParams) (*h if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18346,26 +19174,59 @@ func NewRetrieveRequest(server string, monitorId string, params *RetrieveParams) if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18419,26 +19280,59 @@ func NewUpdateRequestWithBody(server string, monitorId string, params *UpdatePar if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18565,26 +19459,59 @@ func NewChangesRequest(server string, monitorId string, params *ChangesParams) ( if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18632,26 +19559,59 @@ func NewRetrieveChangeRequest(server string, monitorId string, changeId string, if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18692,26 +19652,59 @@ func NewRunNowRequest(server string, monitorId string, params *RunNowParams) (*h if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18804,28 +19797,61 @@ func NewRunsRequest(server string, monitorId string, params *RunsParams) (*http. return nil, err } - if params != nil { + if params != nil { + + if params.XNotteCacheMode != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Mode", headerParam0) + } + + if params.XNotteCacheMaxAgeMs != nil { + var headerParam1 string + + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } if params.XNotteRequestOrigin != nil { - var headerParam0 string + var headerParam3 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("x-notte-request-origin", headerParam3) } if params.XNotteSdkVersion != nil { - var headerParam1 string + var headerParam4 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18873,26 +19899,59 @@ func NewRetrieveRunRequest(server string, monitorId string, runId string, params if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18933,26 +19992,59 @@ func NewRotateSecretRequest(server string, monitorId string, params *RotateSecre if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -18999,26 +20091,59 @@ func NewNewsSearchRequestWithBody(server string, params *NewsSearchParams, conte if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -19065,26 +20190,59 @@ func NewPeopleEnrichRequestWithBody(server string, params *PeopleEnrichParams, c if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -19131,26 +20289,59 @@ func NewExtractProductEndpointRequestWithBody(server string, params *ExtractProd if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -19158,19 +20349,19 @@ func NewExtractProductEndpointRequestWithBody(server string, params *ExtractProd return req, nil } -// NewScrapeSpecializedDataRequest calls the generic ScrapeSpecializedData builder with application/json body -func NewScrapeSpecializedDataRequest(server string, params *ScrapeSpecializedDataParams, body ScrapeSpecializedDataJSONRequestBody) (*http.Request, error) { +// NewFetchSpecializedDataRequest calls the generic FetchSpecializedData builder with application/json body +func NewFetchSpecializedDataRequest(server string, params *FetchSpecializedDataParams, body FetchSpecializedDataJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader buf, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(buf) - return NewScrapeSpecializedDataRequestWithBody(server, params, "application/json", bodyReader) + return NewFetchSpecializedDataRequestWithBody(server, params, "application/json", bodyReader) } -// NewScrapeSpecializedDataRequestWithBody generates requests for ScrapeSpecializedData with any type of body -func NewScrapeSpecializedDataRequestWithBody(server string, params *ScrapeSpecializedDataParams, contentType string, body io.Reader) (*http.Request, error) { +// NewFetchSpecializedDataRequestWithBody generates requests for FetchSpecializedData with any type of body +func NewFetchSpecializedDataRequestWithBody(server string, params *FetchSpecializedDataParams, contentType string, body io.Reader) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -19178,7 +20369,7 @@ func NewScrapeSpecializedDataRequestWithBody(server string, params *ScrapeSpecia return nil, err } - operationPath := fmt.Sprintf("/data/web/scrape") + operationPath := fmt.Sprintf("/data/web/fetch") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -19197,26 +20388,59 @@ func NewScrapeSpecializedDataRequestWithBody(server string, params *ScrapeSpecia if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -19384,26 +20608,59 @@ func NewStyleguideEndpointRequest(server string, params *StyleguideEndpointParam if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -19436,40 +20693,73 @@ func NewDeliveriesRequestWithBody(server string, params *DeliveriesParams, conte operationPath = "." + operationPath } - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + if params != nil { + + if params.XNotteCacheMode != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Mode", headerParam0) + } + + if params.XNotteCacheMaxAgeMs != nil { + var headerParam1 string + + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string - req.Header.Add("Content-Type", contentType) + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } - if params != nil { + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } if params.XNotteRequestOrigin != nil { - var headerParam0 string + var headerParam3 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("x-notte-request-origin", headerParam3) } if params.XNotteSdkVersion != nil { - var headerParam1 string + var headerParam4 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -19510,26 +20800,59 @@ func NewRetrieveDeliveryRequest(server string, deliveryId string, params *Retrie if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -19608,26 +20931,59 @@ func NewAttemptsRequest(server string, deliveryId string, params *AttemptsParams if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -19692,26 +21048,59 @@ func NewRetryRequestWithBody(server string, deliveryId string, params *RetryPara req.Header.Set("Idempotency-Key", headerParam0) } - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam1) + req.Header.Set("X-Notte-Cache-Mode", headerParam1) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam2 string - headerParam2, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam2) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam2) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam3) + } + + if params.XNotteRequestOrigin != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam4) + } + + if params.XNotteSdkVersion != nil { + var headerParam5 string + + headerParam5, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam5) } } @@ -19758,26 +21147,59 @@ func NewYoutubeEndpointRequestWithBody(server string, params *YoutubeEndpointPar if params != nil { - if params.XNotteRequestOrigin != nil { + if params.XNotteCacheMode != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Mode", runtime.ParamLocationHeader, *params.XNotteCacheMode) if err != nil { return nil, err } - req.Header.Set("x-notte-request-origin", headerParam0) + req.Header.Set("X-Notte-Cache-Mode", headerParam0) } - if params.XNotteSdkVersion != nil { + if params.XNotteCacheMaxAgeMs != nil { var headerParam1 string - headerParam1, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + headerParam1, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Max-Age-Ms", runtime.ParamLocationHeader, *params.XNotteCacheMaxAgeMs) if err != nil { return nil, err } - req.Header.Set("x-notte-sdk-version", headerParam1) + req.Header.Set("X-Notte-Cache-Max-Age-Ms", headerParam1) + } + + if params.XNotteCacheTtlSeconds != nil { + var headerParam2 string + + headerParam2, err = runtime.StyleParamWithLocation("simple", false, "X-Notte-Cache-Ttl-Seconds", runtime.ParamLocationHeader, *params.XNotteCacheTtlSeconds) + if err != nil { + return nil, err + } + + req.Header.Set("X-Notte-Cache-Ttl-Seconds", headerParam2) + } + + if params.XNotteRequestOrigin != nil { + var headerParam3 string + + headerParam3, err = runtime.StyleParamWithLocation("simple", false, "x-notte-request-origin", runtime.ParamLocationHeader, *params.XNotteRequestOrigin) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-request-origin", headerParam3) + } + + if params.XNotteSdkVersion != nil { + var headerParam4 string + + headerParam4, err = runtime.StyleParamWithLocation("simple", false, "x-notte-sdk-version", runtime.ParamLocationHeader, *params.XNotteSdkVersion) + if err != nil { + return nil, err + } + + req.Header.Set("x-notte-sdk-version", headerParam4) } } @@ -26773,6 +28195,12 @@ type ClientWithResponsesInterface interface { BrandRetrieveWithResponse(ctx context.Context, params *BrandRetrieveParams, body BrandRetrieveJSONRequestBody, reqEditors ...RequestEditorFn) (*BrandRetrieveResult, error) + // InvalidateWithResponse request + InvalidateWithResponse(ctx context.Context, params *InvalidateParams, reqEditors ...RequestEditorFn) (*InvalidateResult, error) + + // GetPolicyWithResponse request + GetPolicyWithResponse(ctx context.Context, params *GetPolicyParams, reqEditors ...RequestEditorFn) (*GetPolicyResult, error) + // MapsSearchWithBodyWithResponse request with any body MapsSearchWithBodyWithResponse(ctx context.Context, params *MapsSearchParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MapsSearchResult, error) @@ -26842,10 +28270,10 @@ type ClientWithResponsesInterface interface { ExtractProductEndpointWithResponse(ctx context.Context, params *ExtractProductEndpointParams, body ExtractProductEndpointJSONRequestBody, reqEditors ...RequestEditorFn) (*ExtractProductEndpointResult, error) - // ScrapeSpecializedDataWithBodyWithResponse request with any body - ScrapeSpecializedDataWithBodyWithResponse(ctx context.Context, params *ScrapeSpecializedDataParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ScrapeSpecializedDataResult, error) + // FetchSpecializedDataWithBodyWithResponse request with any body + FetchSpecializedDataWithBodyWithResponse(ctx context.Context, params *FetchSpecializedDataParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*FetchSpecializedDataResult, error) - ScrapeSpecializedDataWithResponse(ctx context.Context, params *ScrapeSpecializedDataParams, body ScrapeSpecializedDataJSONRequestBody, reqEditors ...RequestEditorFn) (*ScrapeSpecializedDataResult, error) + FetchSpecializedDataWithResponse(ctx context.Context, params *FetchSpecializedDataParams, body FetchSpecializedDataJSONRequestBody, reqEditors ...RequestEditorFn) (*FetchSpecializedDataResult, error) // StyleguideEndpointWithResponse request StyleguideEndpointWithResponse(ctx context.Context, params *StyleguideEndpointParams, reqEditors ...RequestEditorFn) (*StyleguideEndpointResult, error) @@ -27410,6 +28838,52 @@ func (r BrandRetrieveResult) StatusCode() int { return 0 } +type InvalidateResult struct { + Body []byte + HTTPResponse *http.Response + JSON200 *map[string]bool + JSON422 *HTTPValidationError +} + +// Status returns HTTPResponse.Status +func (r InvalidateResult) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r InvalidateResult) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetPolicyResult struct { + Body []byte + HTTPResponse *http.Response + JSON200 *map[string]interface{} + JSON422 *HTTPValidationError +} + +// Status returns HTTPResponse.Status +func (r GetPolicyResult) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetPolicyResult) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type MapsSearchResult struct { Body []byte HTTPResponse *http.Response @@ -27846,7 +29320,7 @@ func (r ExtractProductEndpointResult) StatusCode() int { return 0 } -type ScrapeSpecializedDataResult struct { +type FetchSpecializedDataResult struct { Body []byte HTTPResponse *http.Response JSON200 *ScrapeResponse @@ -27854,7 +29328,7 @@ type ScrapeSpecializedDataResult struct { } // Status returns HTTPResponse.Status -func (r ScrapeSpecializedDataResult) Status() string { +func (r FetchSpecializedDataResult) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -27862,7 +29336,7 @@ func (r ScrapeSpecializedDataResult) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r ScrapeSpecializedDataResult) StatusCode() int { +func (r FetchSpecializedDataResult) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } @@ -30159,6 +31633,24 @@ func (c *ClientWithResponses) BrandRetrieveWithResponse(ctx context.Context, par return ParseBrandRetrieveResult(rsp) } +// InvalidateWithResponse request returning *InvalidateResult +func (c *ClientWithResponses) InvalidateWithResponse(ctx context.Context, params *InvalidateParams, reqEditors ...RequestEditorFn) (*InvalidateResult, error) { + rsp, err := c.Invalidate(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseInvalidateResult(rsp) +} + +// GetPolicyWithResponse request returning *GetPolicyResult +func (c *ClientWithResponses) GetPolicyWithResponse(ctx context.Context, params *GetPolicyParams, reqEditors ...RequestEditorFn) (*GetPolicyResult, error) { + rsp, err := c.GetPolicy(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetPolicyResult(rsp) +} + // MapsSearchWithBodyWithResponse request with arbitrary body returning *MapsSearchResult func (c *ClientWithResponses) MapsSearchWithBodyWithResponse(ctx context.Context, params *MapsSearchParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MapsSearchResult, error) { rsp, err := c.MapsSearchWithBody(ctx, params, contentType, body, reqEditors...) @@ -30378,21 +31870,21 @@ func (c *ClientWithResponses) ExtractProductEndpointWithResponse(ctx context.Con return ParseExtractProductEndpointResult(rsp) } -// ScrapeSpecializedDataWithBodyWithResponse request with arbitrary body returning *ScrapeSpecializedDataResult -func (c *ClientWithResponses) ScrapeSpecializedDataWithBodyWithResponse(ctx context.Context, params *ScrapeSpecializedDataParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ScrapeSpecializedDataResult, error) { - rsp, err := c.ScrapeSpecializedDataWithBody(ctx, params, contentType, body, reqEditors...) +// FetchSpecializedDataWithBodyWithResponse request with arbitrary body returning *FetchSpecializedDataResult +func (c *ClientWithResponses) FetchSpecializedDataWithBodyWithResponse(ctx context.Context, params *FetchSpecializedDataParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*FetchSpecializedDataResult, error) { + rsp, err := c.FetchSpecializedDataWithBody(ctx, params, contentType, body, reqEditors...) if err != nil { return nil, err } - return ParseScrapeSpecializedDataResult(rsp) + return ParseFetchSpecializedDataResult(rsp) } -func (c *ClientWithResponses) ScrapeSpecializedDataWithResponse(ctx context.Context, params *ScrapeSpecializedDataParams, body ScrapeSpecializedDataJSONRequestBody, reqEditors ...RequestEditorFn) (*ScrapeSpecializedDataResult, error) { - rsp, err := c.ScrapeSpecializedData(ctx, params, body, reqEditors...) +func (c *ClientWithResponses) FetchSpecializedDataWithResponse(ctx context.Context, params *FetchSpecializedDataParams, body FetchSpecializedDataJSONRequestBody, reqEditors ...RequestEditorFn) (*FetchSpecializedDataResult, error) { + rsp, err := c.FetchSpecializedData(ctx, params, body, reqEditors...) if err != nil { return nil, err } - return ParseScrapeSpecializedDataResult(rsp) + return ParseFetchSpecializedDataResult(rsp) } // StyleguideEndpointWithResponse request returning *StyleguideEndpointResult @@ -31827,6 +33319,72 @@ func ParseBrandRetrieveResult(rsp *http.Response) (*BrandRetrieveResult, error) return response, nil } +// ParseInvalidateResult parses an HTTP response from a InvalidateWithResponse call +func ParseInvalidateResult(rsp *http.Response) (*InvalidateResult, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &InvalidateResult{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest map[string]bool + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: + var dest HTTPValidationError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON422 = &dest + + } + + return response, nil +} + +// ParseGetPolicyResult parses an HTTP response from a GetPolicyWithResponse call +func ParseGetPolicyResult(rsp *http.Response) (*GetPolicyResult, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetPolicyResult{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest map[string]interface{} + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: + var dest HTTPValidationError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON422 = &dest + + } + + return response, nil +} + // ParseMapsSearchResult parses an HTTP response from a MapsSearchWithResponse call func ParseMapsSearchResult(rsp *http.Response) (*MapsSearchResult, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -32447,15 +34005,15 @@ func ParseExtractProductEndpointResult(rsp *http.Response) (*ExtractProductEndpo return response, nil } -// ParseScrapeSpecializedDataResult parses an HTTP response from a ScrapeSpecializedDataWithResponse call -func ParseScrapeSpecializedDataResult(rsp *http.Response) (*ScrapeSpecializedDataResult, error) { +// ParseFetchSpecializedDataResult parses an HTTP response from a FetchSpecializedDataWithResponse call +func ParseFetchSpecializedDataResult(rsp *http.Response) (*FetchSpecializedDataResult, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ScrapeSpecializedDataResult{ + response := &FetchSpecializedDataResult{ Body: bodyBytes, HTTPResponse: rsp, } diff --git a/scripts/endpoint-coverage.txt b/scripts/endpoint-coverage.txt index 18076d7..348dc0e 100644 --- a/scripts/endpoint-coverage.txt +++ b/scripts/endpoint-coverage.txt @@ -94,10 +94,12 @@ skip POST /data/monitors/{monitor_id}/webhook/rotate-secret # Data API commands skip POST /data/news/search # Data API commands are not yet exposed by the CLI skip POST /data/people/enrich # Data API commands are not yet exposed by the CLI skip POST /data/products/extract # Data API commands are not yet exposed by the CLI -skip POST /data/web/scrape # Data API commands are not yet exposed by the CLI +skip POST /data/web/fetch # Data API commands are not yet exposed by the CLI skip GET /data/web/styleguide # Data API commands are not yet exposed by the CLI skip POST /data/webhooks/deliveries # Data API commands are not yet exposed by the CLI skip GET /data/webhooks/deliveries/{delivery_id} # Data API commands are not yet exposed by the CLI skip GET /data/webhooks/deliveries/{delivery_id}/attempts # Data API commands are not yet exposed by the CLI skip POST /data/webhooks/deliveries/{delivery_id}/retry # Data API commands are not yet exposed by the CLI skip POST /data/youtube/extract # Data API commands are not yet exposed by the CLI +skip GET /data/cache # Data API commands are not yet exposed by the CLI +skip DELETE /data/cache # Data API commands are not yet exposed by the CLI From 1c0d4b99655349aa10a1b23503fdf0faed33ea1a Mon Sep 17 00:00:00 2001 From: Andrea Pinto Date: Sun, 18 Oct 2026 02:05:10 +0200 Subject: [PATCH 2/6] Sync local browser web storage into profiles --- README.md | 64 ++-- go.mod | 5 +- go.sum | 30 ++ internal/api/profile_state.go | 63 ++++ internal/api/profile_state_test.go | 72 +++++ internal/browser/cdp_state.go | 218 ++++++++++++++ internal/browser/cdp_state_test.go | 169 +++++++++++ internal/browser/cookies.go | 37 ++- internal/browser/cookies_test.go | 42 +++ internal/browser/storage.go | 353 +++++++++++++++++++++++ internal/browser/storage_test.go | 216 ++++++++++++++ internal/cmd/profiles_state_sync_test.go | 155 ++++++++++ internal/cmd/profiles_sync.go | 122 +++++--- internal/cmd/profiles_sync_test.go | 18 +- 14 files changed, 1485 insertions(+), 79 deletions(-) create mode 100644 internal/api/profile_state.go create mode 100644 internal/api/profile_state_test.go create mode 100644 internal/browser/cdp_state.go create mode 100644 internal/browser/cdp_state_test.go create mode 100644 internal/browser/storage.go create mode 100644 internal/browser/storage_test.go create mode 100644 internal/cmd/profiles_state_sync_test.go diff --git a/README.md b/README.md index 7a233e7..4a48cdf 100644 --- a/README.md +++ b/README.md @@ -337,27 +337,49 @@ notte profiles delete --profile-id # Delete a profile notte profiles sync # Copy your local browser login into a profile ``` -`profiles sync` reads the cookies from a local Firefox, Chrome, Brave, Edge, -Chromium, Arc or Helium profile and uploads them into a Notte profile, so remote -sessions start already logged in. The cookies are read from a copy of the -browser's own files on your machine (and, for the Chromium browsers, decrypted -with the key from your OS keychain); nothing is sent until you confirm, and -`--domain` limits the sync to the sites you name. - -```bash -notte profiles sync # pick a browser profile, create a Notte profile -notte profiles sync --domain github.com --domain mail.google.com # only these sites (and subdomains) -notte profiles sync --profile-id # refresh an existing profile after a re-login -notte profiles sync --browser brave --local-profile Work # choose the source without the prompt -``` - -Then start a session with it: `notte sessions start --profile-id `. Re-run -`sync` whenever the local login changes. Supported on macOS and Linux (Arc and -Helium on macOS only); for the -Chromium browsers the first run asks permission to read the browser key from your -OS keychain (Firefox stores cookies in the clear and needs no such permission). -Scoped syncs into an existing profile may want `--mode append` so other sites' -cookies are kept. +`profiles sync` copies cookies and localStorage from a local Firefox, Chrome, +Brave, Edge, Chromium, Arc or Helium profile into a Notte profile. This includes +logins stored only in localStorage, even when the site has no cookies. Browser +files are read through private temporary copies; encrypted cookies are decrypted +locally. Nothing is uploaded until you confirm (or pass `--yes`). `--domain` +restricts cookies and storage to the named hosts and their subdomains. + +```bash +notte profiles sync # choose a local browser profile +notte profiles sync --domain bsky.app # include storage-only logins +notte profiles sync --browser brave --local-profile Work # choose the source explicitly +notte profiles sync --profile-id --domain bsky.app # refresh an existing profile +notte profiles sync --cdp-url http://127.0.0.1:9222 --domain bsky.app +``` + +Start a remote session with `notte sessions start --profile-id `. Re-run sync +when a local login changes. Disk reading is supported on macOS and Linux (Arc +and Helium on macOS only). Chromium cookie decryption may ask for OS keychain +access. Close the source browser and retry if its files change during a copy. + +For live **sessionStorage**, use `--cdp-url` with a Chromium browser already +running with remote debugging enabled in a dedicated profile. Sign into the +site there and keep its tab open. Accepts a debugger HTTP origin or a **page** +WebSocket URL; it reads matching top-level tabs without navigating them. Tabs +with conflicting state for the same origin are rejected rather than combining +different accounts. Disk sync cannot capture in-memory sessionStorage and leaves +previously stored sessionStorage unchanged. + +Storage imports require an API deployment supporting `POST /profiles/{id}/state`. +Older deployments return an explicit unsupported-server error; the CLI never +silently drops localStorage to fall back to cookies. Cookie-only snapshots keep +using the existing cookies API. For storage imports, `--mode replace` replaces +the captured state classes within `--domain` (or all sites without a scope), and +`--mode append` merges cookie identities and storage keys. The cookie-only API +replaces the whole cookie set in replace mode, so use append to preserve other +sites' cookies. Avoid importing into a profile while a remote browser is saving +its state into that same profile. + +The snapshot limit is 8 MiB. Values are never printed in summaries or JSON +output. Unreadable or undecryptable selected state fails before upload. IndexedDB, +partitioned/container storage, browser extensions, passkeys and device-bound +credentials are not transferred. Some sites also bind sessions to an IP or +require a new sign-in; copying browser state cannot guarantee every site's login. ### Files diff --git a/go.mod b/go.mod index 533d642..b9557c8 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,14 @@ go 1.25.5 require ( github.com/99designs/keyring v1.2.2 + github.com/golang/snappy v0.0.4 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 github.com/muesli/termenv v0.16.0 github.com/oapi-codegen/runtime v1.1.2 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 + github.com/syndtr/goleveldb v1.0.0 golang.org/x/sys v0.47.0 golang.org/x/term v0.3.0 modernc.org/sqlite v1.59.0 @@ -21,7 +25,6 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect diff --git a/go.sum b/go.sum index 65d9ce0..6542094 100644 --- a/go.sum +++ b/go.sum @@ -18,16 +18,25 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvsekhvalnov/jose2go v1.7.0 h1:bnQc8+GMnidJZA8zc6lLEAb4xNrIqHwO+9TzqvtQZPo= github.com/dvsekhvalnov/jose2go v1.7.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= @@ -48,6 +57,11 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -67,20 +81,36 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/profile_state.go b/internal/api/profile_state.go new file mode 100644 index 0000000..56a1b12 --- /dev/null +++ b/internal/api/profile_state.go @@ -0,0 +1,63 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// BrowserStateUploadRequest uses the cloud profile's existing browser-state +// format. Domain-scoped replace leaves unrelated cloud logins unchanged. +type BrowserStateUploadRequest struct { + Cookies []Cookie `json:"cookies"` + LocalStorage map[string]map[string]string `json:"local_storage"` + SessionStorage *map[string]map[string]string `json:"session_storage,omitempty"` + Domains []string `json:"domains,omitempty"` + Mode string `json:"mode"` +} + +type BrowserStateUploadResponse struct { + Success bool `json:"success"` + CookiesCount int `json:"cookies_count"` + LocalStorageOrigins int `json:"local_storage_origins"` + SessionStorageOrigins int `json:"session_storage_origins"` +} + +func (c *NotteClient) ImportProfileState(ctx context.Context, id string, state BrowserStateUploadRequest) (*BrowserStateUploadResponse, int, error) { + body, err := json.Marshal(state) + if err != nil { + return nil, 0, fmt.Errorf("cannot encode browser state") + } + if len(body) > 8*1024*1024 { + return nil, 0, fmt.Errorf("browser state exceeds the 8 MiB import limit") + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL(), "/")+"/profiles/"+url.PathEscape(id)+"/state", bytes.NewReader(body)) + if err != nil { + return nil, 0, err + } + req.Header.Set("Content-Type", "application/json") + // Redirects must never forward a body containing browser credentials. + httpClient := *c.HTTPClient() + httpClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse } + resp, err := httpClient.Do(req) + if err != nil { + return nil, 0, fmt.Errorf("profile state import could not be confirmed; inspect the profile before retrying") + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusMethodNotAllowed { + return nil, resp.StatusCode, fmt.Errorf("this API does not support browser-state import yet; update the server before syncing storage-based logins") + } + if resp.StatusCode != http.StatusOK { + return nil, resp.StatusCode, fmt.Errorf("profile state import failed (HTTP %d); browser values were not printed", resp.StatusCode) + } + var result BrowserStateUploadResponse + if err := json.NewDecoder(io.LimitReader(resp.Body, 65536)).Decode(&result); err != nil || !result.Success { + return nil, resp.StatusCode, fmt.Errorf("profile state import returned an invalid confirmation") + } + return &result, resp.StatusCode, nil +} diff --git a/internal/api/profile_state_test.go b/internal/api/profile_state_test.go new file mode 100644 index 0000000..8a0cb31 --- /dev/null +++ b/internal/api/profile_state_test.go @@ -0,0 +1,72 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestImportProfileStorageOnly(t *testing.T) { + for _, live := range []bool{false, true} { + t.Run(fmt.Sprint(live), func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer synthetic-key" { + t.Error("missing workspace authentication") + } + if r.URL.Path != "/profiles/notte-profile-0123456789abcdef/state" { + t.Error("wrong import route") + } + var body map[string]json.RawMessage + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + if _, ok := body["session_storage"]; ok != live { + t.Error("uncaptured session storage must be omitted; captured empty state must be explicit") + } + _, _ = w.Write([]byte(`{"success":true,"cookies_count":0,"local_storage_origins":1,"session_storage_origins":0}`)) + })) + defer server.Close() + client, err := NewClientWithURL("synthetic-key", server.URL, "") + if err != nil { + t.Fatal(err) + } + state := BrowserStateUploadRequest{Cookies: []Cookie{}, LocalStorage: map[string]map[string]string{"https://bsky.app": {"BSKY_STORAGE": "synthetic-private-token"}}, Mode: "replace"} + if live { + sessions := map[string]map[string]string{} + state.SessionStorage = &sessions + } + response, status, err := client.ImportProfileState(context.Background(), "notte-profile-0123456789abcdef", state) + if err != nil || status != 200 || response.LocalStorageOrigins != 1 { + t.Fatalf("import failed: %v", err) + } + }) + } +} + +func TestImportProfileStateNeverLeaksBodyOrFollowsRedirects(t *testing.T) { + for _, code := range []int{307, 308, 400, 401, 403, 404, 405, 422, 500} { + t.Run(fmt.Sprint(code), func(t *testing.T) { + forwarded := false + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { forwarded = true; w.WriteHeader(200) })) + defer target.Close() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", target.URL) + w.WriteHeader(code) + _, _ = w.Write([]byte("synthetic-private-token")) + })) + defer server.Close() + client, err := NewClientWithURL("synthetic-key", server.URL, "") + if err != nil { + t.Fatal(err) + } + _, _, err = client.ImportProfileState(context.Background(), "profile", BrowserStateUploadRequest{LocalStorage: map[string]map[string]string{"https://bsky.app": {"token": "synthetic-private-token"}}}) + if err == nil || strings.Contains(err.Error(), "synthetic-private-token") || forwarded { + t.Fatal("import failure leaked or forwarded browser credentials") + } + }) + } +} diff --git a/internal/browser/cdp_state.go b/internal/browser/cdp_state.go new file mode 100644 index 0000000..719e241 --- /dev/null +++ b/internal/browser/cdp_state.go @@ -0,0 +1,218 @@ +package browser + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "net/http" + "net/url" + "strings" + "time" + + "github.com/gorilla/websocket" +) + +type cdpConnection struct { + conn *websocket.Conn + id int +} + +func (c *cdpConnection) call(method string, params any, result any) error { + c.id++ + if err := c.conn.WriteJSON(map[string]any{"id": c.id, "method": method, "params": params}); err != nil { + return fmt.Errorf("could not send browser snapshot request") + } + for { + var message struct { + ID int `json:"id"` + Result json.RawMessage `json:"result"` + Error *struct { + Code int `json:"code"` + } `json:"error"` + } + if err := c.conn.ReadJSON(&message); err != nil { + return fmt.Errorf("browser disconnected or snapshot timed out; nothing was uploaded") + } + if message.ID != c.id { + continue + } + if message.Error != nil { + return fmt.Errorf("browser refused snapshot operation %s", method) + } + if err := json.Unmarshal(message.Result, result); err != nil { + return fmt.Errorf("invalid browser snapshot response") + } + return nil + } +} + +const readWebStorage = `(() => JSON.stringify({origin:location.origin,local:Object.fromEntries(Object.entries(localStorage)),session:Object.fromEntries(Object.entries(sessionStorage))}))()` + +// ReadCDPState reads live first-party contexts without navigating or modifying +// pages. Distinct tab sessions for one origin cannot fit the cloud profile +// schema: conflicting values fail rather than silently selecting an account. +func ReadCDPState(ctx context.Context, endpoint string, domains []string) (State, error) { + state := State{LocalStorage: map[string]map[string]string{}, SessionStorage: map[string]map[string]string{}} + u, err := url.Parse(endpoint) + if err != nil || u.User != nil { + return State{}, fmt.Errorf("invalid --cdp-url") + } + var targets []struct { + Type string `json:"type"` + URL string `json:"url"` + WebSocket string `json:"webSocketDebuggerUrl"` + } + if u.Scheme == "ws" || u.Scheme == "wss" { + targets = append(targets, struct { + Type string `json:"type"` + URL string `json:"url"` + WebSocket string `json:"webSocketDebuggerUrl"` + }{Type: "page", WebSocket: endpoint}) + } else { + if (u.Scheme != "http" && u.Scheme != "https") || u.Hostname() == "" || u.RawQuery != "" || u.Fragment != "" || u.Path != "" && u.Path != "/" { + return State{}, fmt.Errorf("use a CDP HTTP origin or page WebSocket URL") + } + request, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(endpoint, "/")+"/json/list", nil) + if err != nil { + return State{}, err + } + client := &http.Client{Timeout: 15 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }} + response, err := client.Do(request) + if err != nil { + return State{}, fmt.Errorf("cannot connect to browser debugger; enable remote debugging in a dedicated browser profile") + } + defer func() { _ = response.Body.Close() }() + if response.StatusCode != http.StatusOK { + return State{}, fmt.Errorf("browser debugger did not return a target list") + } + if err := json.NewDecoder(http.MaxBytesReader(nil, response.Body, MaxStateBytes)).Decode(&targets); err != nil { + return State{}, fmt.Errorf("invalid browser target list") + } + } + seenCookies := map[string]Cookie{} + pages := 0 + for _, target := range targets { + if target.Type != "page" || target.WebSocket == "" { + continue + } + if target.URL != "" { + if _, ok := storageOrigin(target.URL, domains); !ok { + // Target URLs include paths, but storageOrigin deliberately accepts + // origins only. Normalize the URL before selecting the page. + pageURL, e := url.Parse(target.URL) + if e != nil { + continue + } + pageURL.Path = "" + pageURL.RawQuery = "" + pageURL.Fragment = "" + pageURL.RawPath = "" + if _, ok = storageOrigin(pageURL.String(), domains); !ok { + continue + } + } + } + pages++ + if err := readTarget(ctx, target.WebSocket, domains, &state, seenCookies); err != nil { + return State{}, err + } + } + if pages == 0 { + return State{}, fmt.Errorf("no open HTTP(S) page matches --domain; open the signed-in site and retry") + } + for _, cookie := range seenCookies { + state.Cookies = append(state.Cookies, cookie) + } + return state, checkStateSize(state) +} + +func readTarget(ctx context.Context, endpoint string, domains []string, state *State, cookies map[string]Cookie) error { + conn, _, err := websocket.DefaultDialer.DialContext(ctx, endpoint, nil) + if err != nil { + return fmt.Errorf("cannot connect to browser page debugger") + } + defer func() { _ = conn.Close() }() + stopCancellation := context.AfterFunc(ctx, func() { _ = conn.Close() }) + defer stopCancellation() + deadline := time.Now().Add(30 * time.Second) + if d, ok := ctx.Deadline(); ok && d.Before(deadline) { + deadline = d + } + if err := conn.SetReadDeadline(deadline); err != nil { + return err + } + if err := conn.SetWriteDeadline(deadline); err != nil { + return err + } + conn.SetReadLimit(MaxStateBytes) + c := cdpConnection{conn: conn} + var cookieResult struct { + Cookies []struct { + Name string `json:"name"` + Value string `json:"value"` + Domain string `json:"domain"` + Path string `json:"path"` + Secure bool `json:"secure"` + HTTPOnly bool `json:"httpOnly"` + SameSite string `json:"sameSite"` + Expires float64 `json:"expires"` + Session bool `json:"session"` + PartitionKey json.RawMessage `json:"partitionKey"` + } `json:"cookies"` + } + if err := c.call("Network.getAllCookies", map[string]any{}, &cookieResult); err != nil { + return err + } + for _, raw := range cookieResult.Cookies { + if len(raw.PartitionKey) > 0 && string(raw.PartitionKey) != "null" { + continue + } + if !domainMatches(raw.Domain, normalizeDomains(domains)) { + continue + } + if !raw.Session && raw.Expires > 0 && raw.Expires <= float64(time.Now().Unix()) { + continue + } + cookie := Cookie{Name: raw.Name, Value: raw.Value, Domain: raw.Domain, Path: raw.Path, Secure: raw.Secure, HTTPOnly: raw.HTTPOnly, SameSite: raw.SameSite, Expires: raw.Expires, Session: raw.Session} + key := raw.Domain + "\x00" + raw.Path + "\x00" + raw.Name + if prior, ok := cookies[key]; ok && prior != cookie { + return fmt.Errorf("open browser contexts have conflicting cookies; select a single account and retry") + } + cookies[key] = cookie + } + var evaluated struct { + Result struct { + Value string `json:"value"` + } `json:"result"` + Exception json.RawMessage `json:"exceptionDetails"` + } + if err := c.call("Runtime.evaluate", map[string]any{"expression": readWebStorage, "returnByValue": true}, &evaluated); err != nil { + return err + } + if len(evaluated.Exception) > 0 { + return fmt.Errorf("browser storage is unavailable on a selected page") + } + var snapshot struct { + Origin string `json:"origin"` + Local map[string]string `json:"local"` + Session map[string]string `json:"session"` + } + if err := json.Unmarshal([]byte(evaluated.Result.Value), &snapshot); err != nil { + return fmt.Errorf("invalid browser storage snapshot") + } + origin, allowed := storageOrigin(snapshot.Origin, domains) + if !allowed { + return fmt.Errorf("selected page changed origin during sync; retry") + } + for _, item := range []struct { + target map[string]map[string]string + source map[string]string + }{{state.LocalStorage, snapshot.Local}, {state.SessionStorage, snapshot.Session}} { + if prior, ok := item.target[origin]; ok && !maps.Equal(prior, item.source) { + return fmt.Errorf("open tabs have conflicting storage for one origin; select a single account/tab and retry") + } + item.target[origin] = item.source + } + return nil +} diff --git a/internal/browser/cdp_state_test.go b/internal/browser/cdp_state_test.go new file mode 100644 index 0000000..6093cb7 --- /dev/null +++ b/internal/browser/cdp_state_test.go @@ -0,0 +1,169 @@ +package browser + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + "github.com/gorilla/websocket" +) + +func cdpFixture(t *testing.T, sessions []string, fail bool) *httptest.Server { + t.Helper() + var server *httptest.Server + upgrader := websocket.Upgrader{CheckOrigin: func(_ *http.Request) bool { return true }} + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/json/list" { + targets := []map[string]string{} + for i := range sessions { + targets = append(targets, map[string]string{"type": "page", "url": fmt.Sprintf("https://bsky.app/feed/%d", i), "webSocketDebuggerUrl": strings.Replace(server.URL, "http:", "ws:", 1) + fmt.Sprintf("/page/%d", i)}) + } + targets = append(targets, map[string]string{"type": "page", "url": "https://unrelated.test/account", "webSocketDebuggerUrl": "ws://127.0.0.1:1/not-allowed"}) + _ = json.NewEncoder(w).Encode(targets) + return + } + var index int + if _, err := fmt.Sscanf(r.URL.Path, "/page/%d", &index); err != nil { + t.Error(err) + return + } + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + defer func() { _ = conn.Close() }() + for { + var req struct { + ID int `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` + } + if err := conn.ReadJSON(&req); err != nil { + return + } + if err := conn.WriteJSON(map[string]any{"method": "Network.event", "params": map[string]any{}}); err != nil { + return + } + if fail { + _ = conn.WriteJSON(map[string]any{"id": req.ID, "error": map[string]any{"code": -1, "message": "synthetic-private-token"}}) + return + } + var result any + switch req.Method { + case "Network.getAllCookies": + result = map[string]any{"cookies": []map[string]any{{"name": "login", "value": "synthetic-cookie", "domain": "bsky.app", "path": "/", "session": true}, {"name": "other", "value": "unrelated-private", "domain": "unrelated.test", "path": "/"}, {"name": "partitioned", "value": "excluded", "domain": "bsky.app", "path": "/", "partitionKey": map[string]any{"topLevelSite": "https://other.test"}}}} + case "Runtime.evaluate": + if req.Params["expression"] != readWebStorage { + t.Error("snapshot must only read storage") + } + snapshot, _ := json.Marshal(map[string]any{"origin": "https://bsky.app", "local": map[string]string{"BSKY_STORAGE": "synthetic-local-login"}, "session": map[string]string{"tab": sessions[index]}}) + result = map[string]any{"result": map[string]any{"type": "string", "value": string(snapshot)}} + default: + t.Error("unexpected mutating CDP command", req.Method) + return + } + if err := conn.WriteJSON(map[string]any{"id": req.ID, "result": result}); err != nil { + return + } + } + })) + t.Cleanup(server.Close) + return server +} + +func TestLiveStateFiltersAndDeduplicatesWithoutMutatingPages(t *testing.T) { + s := cdpFixture(t, []string{"same-tab-state", "same-tab-state"}, false) + state, err := ReadCDPState(context.Background(), s.URL, []string{"bsky.app"}) + if err != nil { + t.Fatal(err) + } + if len(state.Cookies) != 1 || state.Cookies[0].Name != "login" || state.SessionStorage["https://bsky.app"]["tab"] != "same-tab-state" { + t.Fatal("incorrect cookie or storage scope") + } +} + +func TestLiveStateRefusesConflictingTabSessions(t *testing.T) { + s := cdpFixture(t, []string{"account-one", "account-two"}, false) + state, err := ReadCDPState(context.Background(), s.URL, []string{"bsky.app"}) + if err == nil || !strings.Contains(err.Error(), "conflicting") { + t.Fatal("ambiguous account state must not be silently selected") + } + if len(state.LocalStorage) != 0 { + t.Fatal("partial snapshot escaped after failure") + } +} + +func TestLiveStateFailureDoesNotExposeBrowserValues(t *testing.T) { + s := cdpFixture(t, []string{"state"}, true) + _, err := ReadCDPState(context.Background(), s.URL, []string{"bsky.app"}) + if err == nil || strings.Contains(err.Error(), "synthetic-private-token") { + t.Fatal("browser error leaked private data") + } + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) + defer cancel() + _, err = ReadCDPState(ctx, "http://127.0.0.1:1", nil) + if err == nil { + t.Fatal("unreachable browser accepted") + } +} + +func TestLiveStateRefusesInvalidEndpoints(t *testing.T) { + for _, endpoint := range []string{"file:///etc/passwd", "http://user:password@localhost:9222", "http://localhost:9222/?secret=x"} { + if _, err := ReadCDPState(context.Background(), endpoint, nil); err == nil { + t.Fatal("invalid endpoint accepted") + } + } +} + +func TestLiveStateCancellationInterruptsBlockedRead(t *testing.T) { + connected := make(chan struct{}) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + u := websocket.Upgrader{} + conn, err := u.Upgrade(w, r, nil) + if err != nil { + return + } + defer func() { _ = conn.Close() }() + close(connected) + for { + if _, _, err := conn.ReadMessage(); err != nil { + return + } + } + })) + defer server.Close() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + done := make(chan error, 1) + go func() { _, err := ReadCDPState(ctx, strings.Replace(server.URL, "http:", "ws:", 1), nil); done <- err }() + <-connected + cancel() + select { + case err := <-done: + if err == nil { + t.Fatal("cancelled read succeeded") + } + case <-time.After(2 * time.Second): + t.Fatal("cancellation did not interrupt browser snapshot") + } +} + +func TestConcurrentLiveSnapshotsRemainIndependent(t *testing.T) { + s := cdpFixture(t, []string{"same"}, false) + var wait sync.WaitGroup + for range 25 { + wait.Go(func() { + state, err := ReadCDPState(context.Background(), s.URL, []string{"bsky.app"}) + if err != nil || state.LocalStorage["https://bsky.app"]["BSKY_STORAGE"] != "synthetic-local-login" { + t.Error("concurrent snapshot failed", err) + } + }) + } + wait.Wait() +} diff --git a/internal/browser/cookies.go b/internal/browser/cookies.go index e54453b..317c899 100644 --- a/internal/browser/cookies.go +++ b/internal/browser/cookies.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "strings" + "time" _ "modernc.org/sqlite" // pure-Go SQLite driver, no cgo ) @@ -53,16 +54,16 @@ func (p Profile) ReadCookies(domains []string) (ReadResult, error) { if p.Browser.kind == firefoxKind { return p.readFirefoxCookies(domains) } - keys, err := p.Browser.keys() - if err != nil { - return ReadResult{}, err - } - return p.readCookies(keys, domains) + return p.readCookiesWithKeys(p.Browser.keys, domains) } // readCookies is the key-independent core, split out so it can be exercised in // tests with a supplied keyset instead of the OS keystore. func (p Profile) readCookies(keys keyset, domains []string) (ReadResult, error) { + return p.readCookiesWithKeys(func() (keyset, error) { return keys, nil }, domains) +} + +func (p Profile) readCookiesWithKeys(loadKeys func() (keyset, error), domains []string) (ReadResult, error) { dbPath := p.cookieDBPath() if dbPath == "" { return ReadResult{}, fmt.Errorf("no cookie database found for profile %q", p.Name) @@ -97,6 +98,8 @@ func (p Profile) readCookies(keys keyset, domains []string) (ReadResult, error) defer func() { _ = rows.Close() }() result := ReadResult{DBVersion: version} + var keys keyset + keysLoaded := false for rows.Next() { var ( host, name, plainValue, path string @@ -121,6 +124,17 @@ func (p Profile) readCookies(keys keyset, domains []string) (ReadResult, error) if !domainMatches(host, filters) { continue } + if hasExpires != 0 && expiresUTC != 0 && float64(expiresUTC)/1_000_000-chromeEpochOffset <= float64(time.Now().Unix()) { + continue + } + if len(encrypted) > 0 && !keysLoaded { + var err error + keys, err = loadKeys() + if err != nil { + return ReadResult{}, err + } + keysLoaded = true + } value, err := decryptValue(keys, encrypted, plainValue, version) if err != nil { @@ -251,8 +265,7 @@ func decryptValue(keys keyset, encrypted []byte, plain string, version int) (str case "v11": key = keys.v11 default: - // No known version tag: an unencrypted legacy value. - return plain, nil + return "", fmt.Errorf("unsupported encrypted cookie version") } if len(key) == 0 { return "", fmt.Errorf("no decryption key available for %s cookies", prefix) @@ -280,7 +293,10 @@ func decryptValue(keys keyset, encrypted []byte, plain string, version int) (str } // Chrome >= 130 (schema version 24+) prepends the SHA-256 of the cookie's // eTLD+1 to the plaintext. Drop it to recover the real value. - if version >= 24 && len(decrypted) >= 32 { + if version >= 24 { + if len(decrypted) < 32 { + return "", fmt.Errorf("encrypted cookie is missing its domain binding") + } decrypted = decrypted[32:] } return string(decrypted), nil @@ -294,6 +310,11 @@ func stripPKCS7(b []byte) ([]byte, error) { if pad == 0 || pad > aes.BlockSize || pad > len(b) { return nil, fmt.Errorf("invalid padding") } + for _, value := range b[len(b)-pad:] { + if int(value) != pad { + return nil, fmt.Errorf("invalid padding") + } + } return b[:len(b)-pad], nil } diff --git a/internal/browser/cookies_test.go b/internal/browser/cookies_test.go index 0713e41..a9ee801 100644 --- a/internal/browser/cookies_test.go +++ b/internal/browser/cookies_test.go @@ -138,6 +138,48 @@ func TestReadCookiesRoundTrip(t *testing.T) { } } +func TestUnselectedEncryptedCookiesNeverReadKeychain(t *testing.T) { + p := buildCookieDB(t, deriveTestKey(), 24) + called := false + state, err := p.readCookiesWithKeys(func() (keyset, error) { + called = true + return keyset{}, fmt.Errorf("keychain must not be accessed") + }, []string{"plain.com"}) + if err != nil || called || len(state.Cookies) != 1 { + t.Fatal("unselected encrypted cookies triggered keychain access", err) + } +} + +func TestExpiredCookiesAreSkippedBeforeDecryption(t *testing.T) { + p := buildCookieDB(t, deriveTestKey(), 24) + db, err := sql.Open("sqlite", p.cookieDBPath()) + if err != nil { + t.Fatal(err) + } + defer func() { _ = db.Close() }() + if _, err := db.Exec(`UPDATE cookies SET has_expires=1, expires_utc=?`, int64((chromeEpochOffset+1)*1_000_000)); err != nil { + t.Fatal(err) + } + result, err := p.readCookiesWithKeys(func() (keyset, error) { t.Error("expired cookie read the keychain"); return keyset{}, nil }, nil) + if err != nil || len(result.Cookies) != 0 { + t.Fatal("expired cookies were imported", err) + } +} + +func TestUnsupportedCookieEncryptionAndInvalidPaddingFail(t *testing.T) { + if _, err := decryptValue(keyset{}, []byte("v20unsupported-device-bound"), "", 24); err == nil { + t.Fatal("unsupported encryption accepted") + } + for _, raw := range [][]byte{{1, 2}, {0}, {17}, nil} { + if _, err := stripPKCS7(raw); err == nil { + t.Fatal("invalid padding accepted") + } + } + if _, err := decryptValue(keyset{v10: deriveTestKey()}, encryptV10(t, deriveTestKey(), "short", 0), "", 24); err == nil { + t.Fatal("missing domain binding accepted") + } +} + func TestReadCookiesDomainFilter(t *testing.T) { key := deriveTestKey() profile := buildCookieDB(t, key, 24) diff --git a/internal/browser/storage.go b/internal/browser/storage.go new file mode 100644 index 0000000..37b90b1 --- /dev/null +++ b/internal/browser/storage.go @@ -0,0 +1,353 @@ +package browser + +import ( + "bytes" + "database/sql" + "encoding/binary" + "encoding/json" + "fmt" + "net/netip" + "net/url" + "os" + "path/filepath" + "strings" + "unicode/utf16" + "unicode/utf8" + + "github.com/golang/snappy" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/opt" +) + +const MaxStateBytes = 8 * 1024 * 1024 + +// ValidateDomains rejects malformed explicit scopes before any browser read. +// A typo must never turn a scoped sync into an unfiltered credential upload. +func ValidateDomains(domains []string) error { + for _, value := range domains { + host := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(value), ".")) + if _, err := netip.ParseAddr(host); err == nil { + continue + } + if host == "" || len(host) > 253 { + return fmt.Errorf("--domain requires a hostname or IP address without a URL, path or port") + } + for _, label := range strings.Split(host, ".") { + if len(label) == 0 || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' { + return fmt.Errorf("--domain requires a valid hostname or IP address") + } + for _, r := range label { + if (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '-' { + return fmt.Errorf("--domain requires a hostname or IP address without a URL, path or port") + } + } + } + } + return nil +} + +// State is the same origin-scoped snapshot saved by a cloud browser. Cookies +// are mapped to the API shape by the command layer, never printed here. +type State struct { + Cookies []Cookie + LocalStorage map[string]map[string]string + SessionStorage map[string]map[string]string + DecryptFailures int +} + +// ReadState reads a private copy of on-disk state. In-memory sessionStorage is +// available only through ReadCDPState; it is never guessed from stale tab files. +func (p Profile) ReadState(domains []string) (State, error) { + state := State{LocalStorage: map[string]map[string]string{}, SessionStorage: map[string]map[string]string{}} + var err error + if p.Browser.kind == firefoxKind { + state.LocalStorage, err = p.readFirefoxStorage(domains) + } else { + state.LocalStorage, err = p.readChromiumStorage(domains) + } + if err != nil { + return State{}, err + } + cookiePath := p.cookieDBPath() + if p.Browser.kind == firefoxKind { + cookiePath = filepath.Join(p.path, "cookies.sqlite") + } else { + for _, candidate := range []string{filepath.Join(p.path, "Network", "Cookies"), filepath.Join(p.path, "Cookies")} { + if _, err := os.Stat(candidate); err != nil && !os.IsNotExist(err) { + return State{}, fmt.Errorf("could not inspect cookie database; nothing was uploaded") + } + } + } + if _, err := os.Stat(cookiePath); err == nil && cookiePath != "" { + result, err := p.ReadCookies(domains) + if err != nil { + return State{}, err + } + state.Cookies, state.DecryptFailures = result.Cookies, result.DecryptFailures + } else if err != nil && !os.IsNotExist(err) { + return State{}, fmt.Errorf("could not read cookie database") + } + if state.DecryptFailures > 0 { + return State{}, fmt.Errorf("%d selected cookies could not be decrypted; unlock the browser keychain or use --cdp-url; nothing was uploaded", state.DecryptFailures) + } + return state, checkStateSize(state) +} + +func checkStateSize(state State) error { + raw, err := json.Marshal(state) + if err != nil || len(raw) > MaxStateBytes { + return fmt.Errorf("browser state exceeds the 8 MiB sync limit") + } + return nil +} + +func storageOrigin(raw string, domains []string) (string, bool) { + u, err := url.Parse(raw) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") { + return "", false + } + if !domainMatches(u.Hostname(), normalizeDomains(domains)) { + return "", false + } + u.Host = strings.ToLower(u.Host) + u.Path = "" + return u.String(), true +} + +// Chromium stores keys as _\x00. A plain +// first-party StorageKey is its HTTP(S) origin. Partitioned StorageKeys carry +// additional separators and are excluded rather than flattened into logins. +func (p Profile) readChromiumStorage(domains []string) (map[string]map[string]string, error) { + out := map[string]map[string]string{} + source := filepath.Join(p.path, "Local Storage", "leveldb") + if _, err := os.Stat(source); os.IsNotExist(err) { + return out, nil + } else if err != nil { + return nil, fmt.Errorf("could not inspect local storage") + } + tmp, err := os.MkdirTemp("", "notte-storage-") + if err != nil { + return nil, err + } + defer func() { _ = os.RemoveAll(tmp) }() + entries, err := os.ReadDir(source) + if err != nil { + return nil, fmt.Errorf("could not read local storage directory") + } + var copied int64 + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || name == "LOCK" || (name != "CURRENT" && !strings.HasPrefix(name, "MANIFEST-") && !strings.HasSuffix(name, ".log") && !strings.HasSuffix(name, ".ldb") && !strings.HasSuffix(name, ".sst")) { + continue + } + info, err := entry.Info() + if err != nil { + return nil, fmt.Errorf("local storage changed while being copied; close the browser and retry") + } + if !info.Mode().IsRegular() { + return nil, fmt.Errorf("local storage contains an unexpected file") + } + copied += info.Size() + if copied > 512*1024*1024 { + return nil, fmt.Errorf("local storage database is too large; use --cdp-url with --domain") + } + if err := copyFile(filepath.Join(source, name), filepath.Join(tmp, name)); err != nil { + return nil, fmt.Errorf("local storage changed while being copied; close the browser and retry") + } + } + db, err := leveldb.OpenFile(tmp, &opt.Options{ReadOnly: true, ErrorIfMissing: true, Strict: opt.StrictAll}) + if err != nil { + return nil, fmt.Errorf("could not open local storage snapshot; close the browser and retry") + } + defer func() { _ = db.Close() }() + iter := db.NewIterator(nil, nil) + defer iter.Release() + bytesRead := 0 + for iter.Next() { + key := iter.Key() + if len(key) == 0 || key[0] != '_' { + continue + } + originBytes, itemKey, ok := bytes.Cut(key[1:], []byte{0}) + if !ok { + continue + } + origin, allowed := storageOrigin(string(originBytes), domains) + if !allowed { + continue + } + if len(itemKey) > 2*MaxStateBytes+1 || len(iter.Value()) > 2*MaxStateBytes+1 { + return nil, fmt.Errorf("selected local storage exceeds the 8 MiB sync limit") + } + name, err := decodeChromeString(itemKey) + if err != nil { + return nil, err + } + value, err := decodeChromeString(iter.Value()) + if err != nil { + return nil, err + } + bytesRead += len(name) + len(value) + if bytesRead > MaxStateBytes { + return nil, fmt.Errorf("selected local storage exceeds the 8 MiB sync limit") + } + if out[origin] == nil { + out[origin] = map[string]string{} + } + out[origin][name] = value + } + if err := iter.Error(); err != nil { + return nil, fmt.Errorf("local storage snapshot is corrupt; nothing was uploaded") + } + return out, nil +} + +func decodeChromeString(raw []byte) (string, error) { + if len(raw) == 0 { + return "", fmt.Errorf("local storage contains an invalid encoded string") + } + switch raw[0] { + case 1: + runes := make([]rune, len(raw)-1) + for i, b := range raw[1:] { + runes[i] = rune(b) + } + return string(runes), nil + case 0: + return decodeUTF16(raw[1:]) + default: + return "", fmt.Errorf("local storage uses an unsupported string encoding") + } +} + +func decodeUTF16(raw []byte) (string, error) { + if len(raw)%2 != 0 { + return "", fmt.Errorf("local storage contains truncated UTF-16") + } + units := make([]uint16, len(raw)/2) + for i := range units { + units[i] = binary.LittleEndian.Uint16(raw[i*2:]) + } + // JSON/UTF-8 cannot preserve unpaired DOMString surrogates. Refuse instead + // of changing a login token while reporting success. + for i := 0; i < len(units); i++ { + u := units[i] + if u >= 0xd800 && u <= 0xdbff { + if i+1 == len(units) || units[i+1] < 0xdc00 || units[i+1] > 0xdfff { + return "", fmt.Errorf("local storage contains an unsupported unpaired surrogate") + } + i++ + } else if u >= 0xdc00 && u <= 0xdfff { + return "", fmt.Errorf("local storage contains an unsupported unpaired surrogate") + } + } + return string(utf16.Decode(units)), nil +} + +func (p Profile) readFirefoxStorage(domains []string) (map[string]map[string]string, error) { + out := map[string]map[string]string{} + root := filepath.Join(p.path, "storage", "default") + entries, err := os.ReadDir(root) + if os.IsNotExist(err) { + return out, nil + } + if err != nil { + return nil, fmt.Errorf("could not inspect Firefox local storage") + } + for _, entry := range entries { + if !entry.IsDir() || strings.Contains(entry.Name(), "^") { + continue + } // containers and partitioned origins + dbPath := filepath.Join(root, entry.Name(), "ls", "data.sqlite") + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + continue + } else if err != nil { + return nil, fmt.Errorf("could not inspect Firefox local storage") + } + if err := readFirefoxOrigin(dbPath, domains, out); err != nil { + return nil, err + } + if err := checkStateSize(State{LocalStorage: out}); err != nil { + return nil, err + } + } + return out, nil +} + +func readFirefoxOrigin(dbPath string, domains []string, out map[string]map[string]string) error { + tmp, err := os.MkdirTemp("", "notte-storage-") + if err != nil { + return err + } + defer func() { _ = os.RemoveAll(tmp) }() + snapshot, err := snapshotDB(dbPath, tmp) + if err != nil { + return fmt.Errorf("could not snapshot Firefox local storage") + } + db, err := sql.Open("sqlite", "file:"+snapshot+"?mode=ro") + if err != nil { + return err + } + defer func() { _ = db.Close() }() + var rawOrigin string + if err := db.QueryRow(`SELECT origin FROM database`).Scan(&rawOrigin); err != nil { + return fmt.Errorf("unsupported Firefox local storage metadata") + } + origin, allowed := storageOrigin(rawOrigin, domains) + if !allowed { + return nil + } + rows, err := db.Query(`SELECT key, conversion_type, compression_type, value FROM data`) + if err != nil { + // Firefox's earlier LSNG schema stores UTF-8 with a compressed flag. + // Read it without migrating or modifying the source database. + rows, err = db.Query(`SELECT key, 1, compressed, value FROM data`) + if err != nil { + return fmt.Errorf("unsupported Firefox local storage schema; open Firefox to migrate it and retry") + } + } + defer func() { _ = rows.Close() }() + out[origin] = map[string]string{} + var total int + for rows.Next() { + var key string + var conversion, compression int + var raw []byte + if err := rows.Scan(&key, &conversion, &compression, &raw); err != nil { + return fmt.Errorf("could not read Firefox local storage") + } + if compression == 1 { + size, err := snappy.DecodedLen(raw) + if err != nil || size > MaxStateBytes { + return fmt.Errorf("invalid or oversized compressed Firefox storage") + } + raw, err = snappy.Decode(nil, raw) + if err != nil { + return fmt.Errorf("corrupt Firefox local storage") + } + } else if compression != 0 { + return fmt.Errorf("unsupported Firefox storage compression") + } + var value string + switch conversion { + case 0: + value, err = decodeUTF16(raw) + case 1: + if !utf8.Valid(raw) { + return fmt.Errorf("invalid UTF-8 in Firefox storage") + } + value = string(raw) + default: + return fmt.Errorf("unsupported Firefox storage encoding") + } + if err != nil { + return err + } + total += len(key) + len(value) + if total > MaxStateBytes { + return fmt.Errorf("firefox local storage exceeds the sync limit") + } + out[origin][key] = value + } + return rows.Err() +} diff --git a/internal/browser/storage_test.go b/internal/browser/storage_test.go new file mode 100644 index 0000000..0d7b45a --- /dev/null +++ b/internal/browser/storage_test.go @@ -0,0 +1,216 @@ +package browser + +import ( + "database/sql" + "encoding/binary" + "os" + "path/filepath" + "strings" + "testing" + "unicode/utf16" + + "github.com/golang/snappy" + "github.com/syndtr/goleveldb/leveldb" +) + +func encodedUTF16(value string) []byte { + units := utf16.Encode([]rune(value)) + out := make([]byte, len(units)*2) + for i, u := range units { + binary.LittleEndian.PutUint16(out[i*2:], u) + } + return out +} +func chromeText(value string) []byte { return append([]byte{0}, encodedUTF16(value)...) } +func localStorageProfile(t *testing.T) (Profile, *leveldb.DB) { + t.Helper() + profile := Profile{Dir: "Default", Name: "Synthetic", path: t.TempDir()} + dir := filepath.Join(profile.path, "Local Storage", "leveldb") + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + db, err := leveldb.OpenFile(dir, nil) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = db.Close() }) + return profile, db +} + +func putStorage(t *testing.T, db *leveldb.DB, origin, key, value string) { + t.Helper() + rawKey := append([]byte("_"+origin+"\x00"), chromeText(key)...) + if err := db.Put(rawKey, chromeText(value), nil); err != nil { + t.Fatal(err) + } +} + +func TestStorageOnlyProfileIncludesBlueskyLogin(t *testing.T) { + p, db := localStorageProfile(t) + value := `{"session":{"accounts":[{"accessJwt":"synthetic-access","refreshJwt":"synthetic-refresh"}]}}` + putStorage(t, db, "https://bsky.app", "BSKY_STORAGE", value) + putStorage(t, db, "https://unrelated.test", "secret", "excluded") + putStorage(t, db, "https://evilbsky.app", "secret", "excluded") + putStorage(t, db, "https://bsky.app^0https://other.test", "partitioned", "excluded") + state, err := p.ReadState([]string{"bsky.app"}) + if err != nil { + t.Fatal(err) + } + if len(state.Cookies) != 0 || len(state.LocalStorage) != 1 || state.LocalStorage["https://bsky.app"]["BSKY_STORAGE"] != value { + t.Fatal("storage-only login was lost or domain filtering failed") + } +} + +func TestChromiumSnapshotReadsLiveWALAndLatestValues(t *testing.T) { + p, db := localStorageProfile(t) + for i := 0; i < 100; i++ { + putStorage(t, db, "https://bsky.app", "login", strings.Repeat("x", i)) + } + putStorage(t, db, "https://bsky.app", "gone", "old") + if err := db.Delete(append([]byte("_https://bsky.app\x00"), chromeText("gone")...), nil); err != nil { + t.Fatal(err) + } + for i := 0; i < 20; i++ { + state, err := p.ReadState(nil) + if err != nil { + t.Fatal(err) + } + if state.LocalStorage["https://bsky.app"]["login"] != strings.Repeat("x", 99) { + t.Fatal("stale WAL state") + } + if _, ok := state.LocalStorage["https://bsky.app"]["gone"]; ok { + t.Fatal("deleted value was resurrected") + } + } + if err := db.Put([]byte("still-open"), []byte("yes"), nil); err != nil { + t.Fatal("reader modified or locked the original profile") + } +} + +func TestChromeStringEncodings(t *testing.T) { + for _, value := range []string{"", "ascii", "éàø", "日本語", "🔐\x00\n"} { + decoded, err := decodeChromeString(chromeText(value)) + if err != nil || decoded != value { + t.Fatalf("UTF-16 roundtrip: %v", err) + } + } + decoded, err := decodeChromeString([]byte{1, 0xe9, 0xff}) + if err != nil || decoded != "éÿ" { + t.Fatal("Chromium Latin-1 was decoded as UTF-8") + } + for _, bad := range [][]byte{nil, {2, 1}, {0, 1}, {0, 0, 0xd8}, {0, 0, 0xdc}} { + if _, err := decodeChromeString(bad); err == nil { + t.Fatal("invalid string accepted") + } + } +} + +func TestStorageCorruptionAndSizeFailBeforeUpload(t *testing.T) { + p, db := localStorageProfile(t) + if err := db.Put(append([]byte("_https://bsky.app\x00"), chromeText("key")...), []byte{9}, nil); err != nil { + t.Fatal(err) + } + if _, err := p.ReadState([]string{"bsky.app"}); err == nil { + t.Fatal("corrupt selected value accepted") + } + if _, err := p.ReadState([]string{"unrelated.test"}); err != nil { + t.Fatal("unselected corrupt value must not block scoped sync") + } + putStorage(t, db, "https://bsky.app", "key", strings.Repeat("x", MaxStateBytes+1)) + if _, err := p.ReadState(nil); err == nil { + t.Fatal("oversized storage accepted") + } +} + +func TestFirefoxStorageFormats(t *testing.T) { + for _, compressed := range []bool{false, true} { + for _, conversion := range []int{0, 1} { + t.Run(strings.Join([]string{string(rune('0' + conversion)), map[bool]string{false: "plain", true: "snappy"}[compressed]}, "-"), func(t *testing.T) { + p := Profile{Browser: Browser{kind: firefoxKind}, path: t.TempDir()} + dir := filepath.Join(p.path, "storage", "default", "https+++bsky.app", "ls") + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + db, err := sql.Open("sqlite", filepath.Join(dir, "data.sqlite")) + if err != nil { + t.Fatal(err) + } + defer func() { _ = db.Close() }() + for _, stmt := range []string{`CREATE TABLE database(origin TEXT)`, `INSERT INTO database VALUES('https://bsky.app')`, `CREATE TABLE data(key TEXT, conversion_type INTEGER, compression_type INTEGER, value BLOB)`} { + if _, err := db.Exec(stmt); err != nil { + t.Fatal(err) + } + } + value := `{"token":"synthetic-🔐"}` + raw := []byte(value) + if conversion == 0 { + raw = encodedUTF16(value) + } + compression := 0 + if compressed { + raw = snappy.Encode(nil, raw) + compression = 1 + } + if _, err := db.Exec(`INSERT INTO data VALUES(?,?,?,?)`, "BSKY_STORAGE", conversion, compression, raw); err != nil { + t.Fatal(err) + } + state, err := p.ReadState([]string{"bsky.app"}) + if err != nil { + t.Fatal(err) + } + if state.LocalStorage["https://bsky.app"]["BSKY_STORAGE"] != value { + t.Fatal("Firefox login changed during decoding") + } + }) + } + } +} + +func FuzzChromeStringRoundTrip(f *testing.F) { + for _, s := range []string{"", "token", "日本語🔐", "\x00\n\""} { + f.Add(s) + } + f.Fuzz(func(t *testing.T, value string) { + raw := chromeText(value) + decoded, err := decodeChromeString(raw) + if err != nil { + t.Fatal(err) + } + if decoded != string([]rune(value)) { + t.Fatal("UTF-16 value changed") + } + }) +} + +func TestFirefoxLegacyLSNGStorage(t *testing.T) { + for _, compressed := range []bool{false, true} { + p := Profile{Browser: Browser{kind: firefoxKind}, path: t.TempDir()} + dir := filepath.Join(p.path, "storage", "default", "https+++bsky.app", "ls") + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + db, err := sql.Open("sqlite", filepath.Join(dir, "data.sqlite")) + if err != nil { + t.Fatal(err) + } + value := []byte("synthetic-legacy-🔐") + compression := 0 + if compressed { + value = snappy.Encode(nil, value) + compression = 1 + } + for _, statement := range []string{`CREATE TABLE database(origin TEXT)`, `INSERT INTO database VALUES('https://bsky.app')`, `CREATE TABLE data(key TEXT, compressed INTEGER, value BLOB)`} { + if _, err := db.Exec(statement); err != nil { + t.Fatal(err) + } + } + if _, err := db.Exec(`INSERT INTO data VALUES(?,?,?)`, "login", compression, value); err != nil { + t.Fatal(err) + } + _ = db.Close() + state, err := p.ReadState([]string{"bsky.app"}) + if err != nil || state.LocalStorage["https://bsky.app"]["login"] != "synthetic-legacy-🔐" { + t.Fatal("legacy Firefox state changed", err) + } + } +} diff --git a/internal/cmd/profiles_state_sync_test.go b/internal/cmd/profiles_state_sync_test.go new file mode 100644 index 0000000..2f55dea --- /dev/null +++ b/internal/cmd/profiles_state_sync_test.go @@ -0,0 +1,155 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gorilla/websocket" + "github.com/spf13/cobra" + + "github.com/nottelabs/notte-cli/internal/testutil" +) + +func liveSyncFixture(t *testing.T, empty bool) string { + t.Helper() + u := websocket.Upgrader{} + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + conn, err := u.Upgrade(w, r, nil) + if err != nil { + return + } + defer func() { _ = conn.Close() }() + for { + var request struct { + ID int `json:"id"` + Method string `json:"method"` + } + if err := conn.ReadJSON(&request); err != nil { + return + } + var result any = map[string]any{"cookies": []any{}} + if request.Method == "Runtime.evaluate" { + local := map[string]string{} + if !empty { + local["BSKY_STORAGE"] = "synthetic-private-token" + } + snapshot, _ := json.Marshal(map[string]any{"origin": "https://bsky.app", "local": local, "session": map[string]string{}}) + result = map[string]any{"result": map[string]any{"value": string(snapshot)}} + } + if err := conn.WriteJSON(map[string]any{"id": request.ID, "result": result}); err != nil { + return + } + } + })) + t.Cleanup(s.Close) + return strings.Replace(s.URL, "http:", "ws:", 1) +} + +func configureLiveSyncTest(t *testing.T, endpoint string) { + t.Helper() + oldID, oldCDP, oldBrowser, oldLocal := profileID, syncCDPURL, syncBrowser, syncChromeProfile + oldDomains, oldMode, oldName := syncDomains, syncMode, syncProfileName + oldConfirm, oldOutput := skipConfirmation, outputFormat + t.Cleanup(func() { + profileID, syncCDPURL, syncBrowser, syncChromeProfile = oldID, oldCDP, oldBrowser, oldLocal + syncDomains, syncMode, syncProfileName = oldDomains, oldMode, oldName + skipConfirmation, outputFormat = oldConfirm, oldOutput + }) + profileID, syncCDPURL, syncBrowser, syncChromeProfile = "", endpoint, "", "" + syncDomains, syncMode, syncProfileName = []string{"bsky.app"}, "replace", "Synthetic" + skipConfirmation, outputFormat = true, "json" +} + +func TestLiveSyncCommandStorageOnlyAndFailureCleanup(t *testing.T) { + for _, existing := range []bool{false, true} { + for _, status := range []int{200, 404, 422, 500} { + t.Run(fmt.Sprintf("existing-%t-status-%d", existing, status), func(t *testing.T) { + env := testutil.SetupTestEnv(t) + env.SetEnv("NOTTE_API_KEY", "test-key") + configureLiveSyncTest(t, liveSyncFixture(t, false)) + if existing { + profileID = profileIDTest + } + creates, uploads, deletes := 0, 0, 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer test-key" { + t.Error("missing authentication") + } + w.Header().Set("Content-Type", "application/json") + switch { + case r.URL.Path == "/profiles/create": + creates++ + _, _ = w.Write([]byte(profileJSON())) + case strings.HasSuffix(r.URL.Path, "/state"): + uploads++ + var body map[string]json.RawMessage + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Error(err) + } + if _, ok := body["session_storage"]; !ok { + t.Error("live sync must capture session storage explicitly") + } + w.WriteHeader(status) + if status == 200 { + _, _ = w.Write([]byte(`{"success":true,"cookies_count":0,"local_storage_origins":1,"session_storage_origins":1}`)) + } else { + _, _ = w.Write([]byte(`{"detail":"synthetic-private-token"}`)) + } + case r.Method == "DELETE": + deletes++ + _, _ = w.Write([]byte(`{"success":true}`)) + default: + t.Error("unexpected API request", r.Method, r.URL.Path) + } + })) + defer server.Close() + env.SetEnv("NOTTE_API_URL", server.URL) + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + var runErr error + stdout, stderr := testutil.CaptureOutput(func() { runErr = runProfilesSync(cmd, nil) }) + if status == 200 { + if runErr != nil { + t.Fatal(runErr) + } + var metadata map[string]any + if err := json.Unmarshal([]byte(stdout), &metadata); err != nil { + t.Fatal(err) + } + if metadata["profile_id"] != profileIDTest || metadata["cookies_count"] != float64(0) { + t.Fatal("invalid metadata") + } + } else if runErr == nil || strings.Contains(runErr.Error(), "synthetic-private-token") { + t.Fatal("failed import reported success or leaked values") + } + if strings.Contains(stdout+stderr, "synthetic-private-token") { + t.Fatal("browser credentials appeared in output") + } + wantCreates, wantDeletes := 0, 0 + if !existing { + wantCreates = 1 + if status >= 400 && status < 500 { + wantDeletes = 1 + } + } + if creates != wantCreates || deletes != wantDeletes || uploads != 1 { + t.Fatalf("creates=%d deletes=%d uploads=%d", creates, deletes, uploads) + } + }) + } + } +} + +func TestEmptyLiveSyncNeverCreatesProfile(t *testing.T) { + configureLiveSyncTest(t, liveSyncFixture(t, true)) + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + if err := runProfilesSync(cmd, nil); err == nil || !strings.Contains(err.Error(), "no cookies or web storage") { + t.Fatal("empty snapshot must fail before accessing the API") + } +} diff --git a/internal/cmd/profiles_sync.go b/internal/cmd/profiles_sync.go index e835faf..a9f1658 100644 --- a/internal/cmd/profiles_sync.go +++ b/internal/cmd/profiles_sync.go @@ -23,18 +23,21 @@ var ( syncDomains []string syncMode string syncProfileName string + syncCDPURL string ) var profilesSyncCmd = &cobra.Command{ Use: "sync", - Short: "Upload cookies from your local browser into a Notte profile", - Long: "Read the cookies from a local Firefox, Chrome, Brave, Edge, Chromium, Arc or\n" + + Short: "Sync local browser cookies and web storage into a Notte profile", + Long: "Read cookies and localStorage from a local Firefox, Chrome, Brave, Edge, Chromium, Arc or\n" + "Helium profile and upload them into a Notte cloud profile, so remote sessions\n" + "and agents start already logged in. Arc and Helium are supported on macOS only.\n\n" + - "Cookies are read from a copy of the browser's own files on this machine and\n" + + "Browser state is read from a private copy of the browser's files on this machine; cookies are\n" + "decrypted locally. Nothing leaves your computer until you confirm the upload,\n" + "and with --domain you choose exactly which sites are included.\n\n" + - "With --profile-id the cookies go into an existing profile; without it a new\n" + + "With --cdp-url, read a running Chromium browser, including sessionStorage.\n\n" + + "IndexedDB, partitioned storage and device-bound credentials are not transferred.\n\n" + + "With --profile-id, state goes into an existing profile; without it a new\n" + "profile is created and its id is printed for you to reuse next time.", Args: cobra.NoArgs, RunE: runProfilesSync, @@ -45,13 +48,16 @@ func init() { profilesSyncCmd.Flags().StringVar(&profileID, "profile-id", "", "Existing Notte profile to sync into (creates a new one if omitted)") profilesSyncCmd.Flags().StringVar(&syncBrowser, "browser", "", "Browser to read from (firefox, chrome, brave, edge, chromium, arc, helium)") profilesSyncCmd.Flags().StringVar(&syncChromeProfile, "local-profile", "", "Local browser profile, by name or directory (prompts if there is more than one)") - profilesSyncCmd.Flags().StringSliceVar(&syncDomains, "domain", nil, "Only sync cookies for these domains and their subdomains (repeatable)") - profilesSyncCmd.Flags().StringVar(&syncMode, "mode", "replace", "How to apply the cookies: replace the profile's cookies, or append to them") + profilesSyncCmd.Flags().StringSliceVar(&syncDomains, "domain", nil, "Only sync state for these domains and their subdomains (repeatable)") + profilesSyncCmd.Flags().StringVar(&syncMode, "mode", "replace", "Replace captured state or append/update its keys") profilesSyncCmd.Flags().StringVar(&syncProfileName, "name", "", "Name for the new profile when --profile-id is omitted") + profilesSyncCmd.Flags().StringVar(&syncCDPURL, "cdp-url", "", "Read live Chromium cookies, localStorage and sessionStorage from a debugger HTTP origin or page WebSocket URL") + profilesSyncCmd.MarkFlagsMutuallyExclusive("cdp-url", "browser") + profilesSyncCmd.MarkFlagsMutuallyExclusive("cdp-url", "local-profile") } func runProfilesSync(cmd *cobra.Command, args []string) error { - if !browser.SupportedPlatform() { + if syncCDPURL == "" && !browser.SupportedPlatform() { return fmt.Errorf("syncing cookies from a local browser is supported on macOS and Linux only") } if syncMode != "replace" && syncMode != "append" { @@ -60,29 +66,39 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { // An explicit --domain that normalizes to nothing (e.g. \".\" or a blank) // must be rejected rather than silently treated as "all domains", which // would upload every cookie under --yes. - if len(syncDomains) > 0 && !hasValidDomain(syncDomains) { - return fmt.Errorf("--domain was given but contains no valid domain") - } - - profile, err := resolveLocalProfile() - if err != nil { + if err := browser.ValidateDomains(syncDomains); err != nil { return err } - PrintInfo(fmt.Sprintf("Reading cookies from %s profile %q ...", profile.Browser.DisplayName, profile.Name)) - result, err := profile.ReadCookies(syncDomains) + readCtx, readCancel := GetContextWithTimeout(cmd.Context()) + defer readCancel() + var result browser.State + var err error + label := "Live browser" + if syncCDPURL != "" { + result, err = browser.ReadCDPState(readCtx, syncCDPURL, syncDomains) + } else { + profile, resolveErr := resolveLocalProfile() + if resolveErr != nil { + return resolveErr + } + label = fmt.Sprintf("%s - %s", profile.Browser.DisplayName, profile.Name) + PrintInfo(fmt.Sprintf("Reading browser state from %s ...", label)) + result, err = profile.ReadState(syncDomains) + } if err != nil { return err } - if len(result.Cookies) == 0 { - if len(syncDomains) > 0 { - return fmt.Errorf("no cookies found for %s in that profile", strings.Join(syncDomains, ", ")) - } - return fmt.Errorf("no cookies found in that profile") + if len(result.Cookies) == 0 && storageItems(result.LocalStorage) == 0 && storageItems(result.SessionStorage) == 0 { + return fmt.Errorf("no cookies or web storage found for the selected sites") } - cookies := toAPICookies(result.Cookies) - printCookieSummary(result, cookies) + printCookieSummary(browser.ReadResult{Cookies: result.Cookies}, cookies) + PrintInfo(fmt.Sprintf("Web storage: %d localStorage origins, %d sessionStorage origins.", len(result.LocalStorage), len(result.SessionStorage))) + if syncCDPURL == "" { + PrintInfo("Disk sync does not include in-memory sessionStorage; use --cdp-url for live tab state.") + } + PrintInfo("IndexedDB, partitioned storage and device-bound credentials are not transferred.") // Decide where the cookies will go and confirm before touching the network, // so a decline (or a dry run) never creates anything. @@ -91,20 +107,21 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { if target == "" { newName = syncProfileName if newName == "" { - newName = fmt.Sprintf("%s - %s", profile.Browser.DisplayName, profile.Name) + newName = label } target = fmt.Sprintf("a new profile named %q", newName) } - if !confirmSync(len(cookies), target) { + if !confirmStateSync(len(cookies), len(result.LocalStorage), len(result.SessionStorage), target) { return PrintResult("Cancelled.", map[string]any{"cancelled": true}) } + readCancel() + ctx, cancel := GetContextWithTimeout(cmd.Context()) + defer cancel() client, err := GetClient() if err != nil { return err } - ctx, cancel := GetContextWithTimeout(cmd.Context()) - defer cancel() targetID := profileID created := false @@ -117,6 +134,23 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { created = true } + if len(result.LocalStorage) > 0 || len(result.SessionStorage) > 0 { + if cookies == nil { + cookies = []api.Cookie{} + } + var sessions *map[string]map[string]string + if syncCDPURL != "" { + sessions = &result.SessionStorage + } + response, status, importErr := client.ImportProfileState(ctx, targetID, api.BrowserStateUploadRequest{Cookies: cookies, LocalStorage: result.LocalStorage, SessionStorage: sessions, Domains: syncDomains, Mode: syncMode}) + if importErr != nil { + if created && status >= 400 && status < 500 { + _, _ = client.Client().ProfileDeleteWithResponse(ctx, targetID, &api.ProfileDeleteParams{}) + } + return fmt.Errorf("profile %s: %w", targetID, importErr) + } + return PrintResult(fmt.Sprintf("Synced browser state into profile %s (%d cookies, %d localStorage origins, %d sessionStorage origins).", targetID, response.CookiesCount, response.LocalStorageOrigins, response.SessionStorageOrigins), map[string]any{"profile_id": targetID, "created": created, "synced": len(cookies), "cookies_count": response.CookiesCount, "local_storage_origins": response.LocalStorageOrigins, "session_storage_origins": response.SessionStorageOrigins, "mode": syncMode}) + } mode := api.ProfileCookiesImportRequestMode(syncMode) // The cookies are already normalized to the Playwright shape (string // sameSite, Unix-seconds expiry) in toAPICookies, so we declare that shape @@ -154,6 +188,24 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { return nil } +func storageItems(storage map[string]map[string]string) int { + count := 0 + for _, values := range storage { + count += len(values) + } + return count +} + +func confirmStateSync(cookies, local, session int, target string) bool { + if skipConfirmation { + return true + } + _, _ = fmt.Fprintf(os.Stderr, "Upload %d cookies, %d localStorage origins and %d sessionStorage origins into %s in mode %q? [y/N]: ", cookies, local, session, target, syncMode) + reader := bufio.NewReader(os.Stdin) + answer, err := reader.ReadString('\n') + return (err == nil || err == io.EOF) && (strings.EqualFold(strings.TrimSpace(answer), "y") || strings.EqualFold(strings.TrimSpace(answer), "yes")) +} + // resolveLocalProfile picks which local browser profile to read, from flags or // by prompting when the choice is ambiguous. func resolveLocalProfile() (browser.Profile, error) { @@ -202,17 +254,6 @@ func resolveLocalProfile() (browser.Profile, error) { return pickProfile(candidates) } -// hasValidDomain reports whether at least one --domain argument survives the -// same normalization the reader applies (trim, drop a leading dot). -func hasValidDomain(domains []string) bool { - for _, d := range domains { - if strings.TrimPrefix(strings.TrimSpace(d), ".") != "" { - return true - } - } - return false -} - func filterByProfileName(profiles []browser.Profile, query string) []browser.Profile { q := strings.ToLower(query) var matched []browser.Profile @@ -353,13 +394,6 @@ func createProfile(ctx context.Context, client *api.NotteClient, name string) (s return resp.JSON200.ProfileId, nil } -func confirmSync(cookieCount int, target string) bool { - if skipConfirmation { - return true - } - return confirmSyncWithIO(os.Stdin, os.Stderr, cookieCount, target) -} - func confirmSyncWithIO(in io.Reader, out io.Writer, cookieCount int, target string) bool { _, _ = fmt.Fprintf(out, "Upload %d cookies into %s in mode %q? [y/N]: ", cookieCount, target, syncMode) diff --git a/internal/cmd/profiles_sync_test.go b/internal/cmd/profiles_sync_test.go index 8f3c56f..0b3d375 100644 --- a/internal/cmd/profiles_sync_test.go +++ b/internal/cmd/profiles_sync_test.go @@ -85,22 +85,30 @@ func TestFilterByProfileName(t *testing.T) { func TestHasValidDomain(t *testing.T) { valid := [][]string{ {"github.com"}, - {" ", "github.com"}, + {"GitHub.com", " localhost "}, {".github.com"}, + {"127.0.0.1", "::1"}, } for _, in := range valid { - if !hasValidDomain(in) { - t.Errorf("hasValidDomain(%q) = false, want true", in) + if err := browser.ValidateDomains(in); err != nil { + t.Errorf("ValidateDomains(%q): %v", in, err) } } invalid := [][]string{ {" "}, {"."}, {"", " ", "."}, + {"github.com", " "}, + {"https://github.com"}, + {"github.com:443"}, + {"git..com"}, + {"-github.com"}, + {"github.com/path"}, + {"github.com?secret=x"}, } for _, in := range invalid { - if hasValidDomain(in) { - t.Errorf("hasValidDomain(%q) = true, want false", in) + if browser.ValidateDomains(in) == nil { + t.Errorf("ValidateDomains(%q) accepted invalid scope", in) } } } From 99741373e9a4bdd2312373f0b0113eca08fd05bc Mon Sep 17 00:00:00 2001 From: Andrea Pinto Date: Sun, 18 Oct 2026 02:09:25 +0200 Subject: [PATCH 3/6] Align storage upload method with the API operation --- internal/api/profile_state.go | 2 +- internal/api/profile_state_test.go | 4 ++-- internal/cmd/profiles_sync.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/api/profile_state.go b/internal/api/profile_state.go index 56a1b12..2dcd3e8 100644 --- a/internal/api/profile_state.go +++ b/internal/api/profile_state.go @@ -28,7 +28,7 @@ type BrowserStateUploadResponse struct { SessionStorageOrigins int `json:"session_storage_origins"` } -func (c *NotteClient) ImportProfileState(ctx context.Context, id string, state BrowserStateUploadRequest) (*BrowserStateUploadResponse, int, error) { +func (c *NotteClient) ProfileStateSetWithResponse(ctx context.Context, id string, state BrowserStateUploadRequest) (*BrowserStateUploadResponse, int, error) { body, err := json.Marshal(state) if err != nil { return nil, 0, fmt.Errorf("cannot encode browser state") diff --git a/internal/api/profile_state_test.go b/internal/api/profile_state_test.go index 8a0cb31..e3ea5a6 100644 --- a/internal/api/profile_state_test.go +++ b/internal/api/profile_state_test.go @@ -39,7 +39,7 @@ func TestImportProfileStorageOnly(t *testing.T) { sessions := map[string]map[string]string{} state.SessionStorage = &sessions } - response, status, err := client.ImportProfileState(context.Background(), "notte-profile-0123456789abcdef", state) + response, status, err := client.ProfileStateSetWithResponse(context.Background(), "notte-profile-0123456789abcdef", state) if err != nil || status != 200 || response.LocalStorageOrigins != 1 { t.Fatalf("import failed: %v", err) } @@ -63,7 +63,7 @@ func TestImportProfileStateNeverLeaksBodyOrFollowsRedirects(t *testing.T) { if err != nil { t.Fatal(err) } - _, _, err = client.ImportProfileState(context.Background(), "profile", BrowserStateUploadRequest{LocalStorage: map[string]map[string]string{"https://bsky.app": {"token": "synthetic-private-token"}}}) + _, _, err = client.ProfileStateSetWithResponse(context.Background(), "profile", BrowserStateUploadRequest{LocalStorage: map[string]map[string]string{"https://bsky.app": {"token": "synthetic-private-token"}}}) if err == nil || strings.Contains(err.Error(), "synthetic-private-token") || forwarded { t.Fatal("import failure leaked or forwarded browser credentials") } diff --git a/internal/cmd/profiles_sync.go b/internal/cmd/profiles_sync.go index a9f1658..117f239 100644 --- a/internal/cmd/profiles_sync.go +++ b/internal/cmd/profiles_sync.go @@ -142,7 +142,7 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { if syncCDPURL != "" { sessions = &result.SessionStorage } - response, status, importErr := client.ImportProfileState(ctx, targetID, api.BrowserStateUploadRequest{Cookies: cookies, LocalStorage: result.LocalStorage, SessionStorage: sessions, Domains: syncDomains, Mode: syncMode}) + response, status, importErr := client.ProfileStateSetWithResponse(ctx, targetID, api.BrowserStateUploadRequest{Cookies: cookies, LocalStorage: result.LocalStorage, SessionStorage: sessions, Domains: syncDomains, Mode: syncMode}) if importErr != nil { if created && status >= 400 && status < 500 { _, _ = client.Client().ProfileDeleteWithResponse(ctx, targetID, &api.ProfileDeleteParams{}) From 77cc5c7cc47fcf7f53496b639662cbba53b5b45a Mon Sep 17 00:00:00 2001 From: Andrea Pinto Date: Sun, 18 Oct 2026 02:26:26 +0200 Subject: [PATCH 4/6] Preserve uncaptured profile state during sync --- README.md | 13 +++++---- internal/api/profile_state.go | 16 ++++++++-- internal/browser/storage.go | 10 +++++++ internal/browser/storage_test.go | 17 +++++++++++ internal/cmd/profiles_state_sync_test.go | 37 ++++++++++++++++++++++-- internal/cmd/profiles_sync.go | 36 +++++++++++++++++------ 6 files changed, 109 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4a48cdf..32ffe6d 100644 --- a/README.md +++ b/README.md @@ -367,12 +367,13 @@ previously stored sessionStorage unchanged. Storage imports require an API deployment supporting `POST /profiles/{id}/state`. Older deployments return an explicit unsupported-server error; the CLI never -silently drops localStorage to fall back to cookies. Cookie-only snapshots keep -using the existing cookies API. For storage imports, `--mode replace` replaces -the captured state classes within `--domain` (or all sites without a scope), and -`--mode append` merges cookie identities and storage keys. The cookie-only API -replaces the whole cookie set in replace mode, so use append to preserve other -sites' cookies. Avoid importing into a profile while a remote browser is saving +silently drops localStorage to fall back to cookies. Scoped cookie-only syncs +also use the state API to preserve other sites. Unscoped cookie-only disk syncs +remain compatible with the existing cookies API. `--mode replace` replaces +captured state classes within `--domain` (or all sites without a scope). +Live sync replaces web storage only for captured origins, preserving saved +state for closed tabs. `--mode append` merges cookie identities and storage keys. +Avoid importing into a profile while a remote browser is saving its state into that same profile. The snapshot limit is 8 MiB. Values are never printed in summaries or JSON diff --git a/internal/api/profile_state.go b/internal/api/profile_state.go index 2dcd3e8..67010b6 100644 --- a/internal/api/profile_state.go +++ b/internal/api/profile_state.go @@ -19,6 +19,7 @@ type BrowserStateUploadRequest struct { SessionStorage *map[string]map[string]string `json:"session_storage,omitempty"` Domains []string `json:"domains,omitempty"` Mode string `json:"mode"` + StorageScope string `json:"storage_scope,omitempty"` } type BrowserStateUploadResponse struct { @@ -28,14 +29,23 @@ type BrowserStateUploadResponse struct { SessionStorageOrigins int `json:"session_storage_origins"` } -func (c *NotteClient) ProfileStateSetWithResponse(ctx context.Context, id string, state BrowserStateUploadRequest) (*BrowserStateUploadResponse, int, error) { +// Validate checks the final wire representation before a destination is created. +func (state BrowserStateUploadRequest) Validate() error { body, err := json.Marshal(state) if err != nil { - return nil, 0, fmt.Errorf("cannot encode browser state") + return fmt.Errorf("cannot encode browser state") } if len(body) > 8*1024*1024 { - return nil, 0, fmt.Errorf("browser state exceeds the 8 MiB import limit") + return fmt.Errorf("browser state exceeds the 8 MiB import limit") + } + return nil +} + +func (c *NotteClient) ProfileStateSetWithResponse(ctx context.Context, id string, state BrowserStateUploadRequest) (*BrowserStateUploadResponse, int, error) { + if err := state.Validate(); err != nil { + return nil, 0, err } + body, _ := json.Marshal(state) req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL(), "/")+"/profiles/"+url.PathEscape(id)+"/state", bytes.NewReader(body)) if err != nil { return nil, 0, err diff --git a/internal/browser/storage.go b/internal/browser/storage.go index 37b90b1..b79588b 100644 --- a/internal/browser/storage.go +++ b/internal/browser/storage.go @@ -258,6 +258,16 @@ func (p Profile) readFirefoxStorage(domains []string) (map[string]map[string]str if !entry.IsDir() || strings.Contains(entry.Name(), "^") { continue } // containers and partitioned origins + // Firefox encodes scheme://host[:port] as scheme+++host[+port]. + // Filter the directory before touching unrelated databases, then still + // validate the stored origin inside each selected database. + parts := strings.SplitN(entry.Name(), "+++", 2) + if len(parts) != 2 || (parts[0] != "http" && parts[0] != "https") { + continue + } + if _, allowed := storageOrigin(parts[0]+"://"+strings.ReplaceAll(parts[1], "+", ":"), domains); !allowed { + continue + } dbPath := filepath.Join(root, entry.Name(), "ls", "data.sqlite") if _, err := os.Stat(dbPath); os.IsNotExist(err) { continue diff --git a/internal/browser/storage_test.go b/internal/browser/storage_test.go index 0d7b45a..0f20916 100644 --- a/internal/browser/storage_test.go +++ b/internal/browser/storage_test.go @@ -214,3 +214,20 @@ func TestFirefoxLegacyLSNGStorage(t *testing.T) { } } } + +func TestFirefoxScopedSyncSkipsUnrelatedCorruptDatabase(t *testing.T) { + p := Profile{Browser: Browser{kind: firefoxKind}, path: t.TempDir()} + dir := filepath.Join(p.path, "storage", "default", "https+++unrelated.test", "ls") + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "data.sqlite"), []byte("broken database"), 0o600); err != nil { + t.Fatal(err) + } + if _, err := p.ReadState([]string{"bsky.app"}); err != nil { + t.Fatal("unrelated corrupt storage must not block scoped sync", err) + } + if _, err := p.ReadState([]string{"unrelated.test"}); err == nil { + t.Fatal("selected corrupt storage must fail") + } +} diff --git a/internal/cmd/profiles_state_sync_test.go b/internal/cmd/profiles_state_sync_test.go index 2f55dea..a64c9cb 100644 --- a/internal/cmd/profiles_state_sync_test.go +++ b/internal/cmd/profiles_state_sync_test.go @@ -12,10 +12,19 @@ import ( "github.com/gorilla/websocket" "github.com/spf13/cobra" + "github.com/nottelabs/notte-cli/internal/browser" "github.com/nottelabs/notte-cli/internal/testutil" ) func liveSyncFixture(t *testing.T, empty bool) string { + value := "synthetic-private-token" + if empty { + value = "" + } + return liveSyncValueFixture(t, value) +} + +func liveSyncValueFixture(t *testing.T, value string) string { t.Helper() u := websocket.Upgrader{} s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -35,8 +44,8 @@ func liveSyncFixture(t *testing.T, empty bool) string { var result any = map[string]any{"cookies": []any{}} if request.Method == "Runtime.evaluate" { local := map[string]string{} - if !empty { - local["BSKY_STORAGE"] = "synthetic-private-token" + if value != "" { + local["BSKY_STORAGE"] = value } snapshot, _ := json.Marshal(map[string]any{"origin": "https://bsky.app", "local": local, "session": map[string]string{}}) result = map[string]any{"result": map[string]any{"value": string(snapshot)}} @@ -94,6 +103,9 @@ func TestLiveSyncCommandStorageOnlyAndFailureCleanup(t *testing.T) { if _, ok := body["session_storage"]; !ok { t.Error("live sync must capture session storage explicitly") } + if string(body["storage_scope"]) != `"origins"` { + t.Error("live snapshots must preserve storage for closed tabs") + } w.WriteHeader(status) if status == 200 { _, _ = w.Write([]byte(`{"success":true,"cookies_count":0,"local_storage_origins":1,"session_storage_origins":1}`)) @@ -145,6 +157,27 @@ func TestLiveSyncCommandStorageOnlyAndFailureCleanup(t *testing.T) { } } +func TestOversizedFinalRequestNeverCreatesProfile(t *testing.T) { + env := testutil.SetupTestEnv(t) + env.SetEnv("NOTTE_API_KEY", "test-key") + configureLiveSyncTest(t, liveSyncValueFixture(t, strings.Repeat("x", browser.MaxStateBytes-4096))) + // The browser snapshot fits; adding the requested domain scope does not. + syncDomains = make([]string, 1000) + for i := range syncDomains { + syncDomains[i] = "bsky.app" + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Error("oversized request accessed the API", r.URL.Path) + })) + defer server.Close() + env.SetEnv("NOTTE_API_URL", server.URL) + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + if err := runProfilesSync(cmd, nil); err == nil || !strings.Contains(err.Error(), "import limit") { + t.Fatal("final wire size must be checked before profile creation", err) + } +} + func TestEmptyLiveSyncNeverCreatesProfile(t *testing.T) { configureLiveSyncTest(t, liveSyncFixture(t, true)) cmd := &cobra.Command{} diff --git a/internal/cmd/profiles_sync.go b/internal/cmd/profiles_sync.go index 117f239..36fc286 100644 --- a/internal/cmd/profiles_sync.go +++ b/internal/cmd/profiles_sync.go @@ -93,6 +93,13 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { return fmt.Errorf("no cookies or web storage found for the selected sites") } cookies := toAPICookies(result.Cookies) + useStateImport := len(result.LocalStorage) > 0 || len(result.SessionStorage) > 0 || len(syncDomains) > 0 || syncCDPURL != "" + stateRequest := buildStateUpload(result, cookies, syncDomains, syncMode, syncCDPURL != "") + if useStateImport { + if err := stateRequest.Validate(); err != nil { + return err + } + } printCookieSummary(browser.ReadResult{Cookies: result.Cookies}, cookies) PrintInfo(fmt.Sprintf("Web storage: %d localStorage origins, %d sessionStorage origins.", len(result.LocalStorage), len(result.SessionStorage))) if syncCDPURL == "" { @@ -134,15 +141,8 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { created = true } - if len(result.LocalStorage) > 0 || len(result.SessionStorage) > 0 { - if cookies == nil { - cookies = []api.Cookie{} - } - var sessions *map[string]map[string]string - if syncCDPURL != "" { - sessions = &result.SessionStorage - } - response, status, importErr := client.ProfileStateSetWithResponse(ctx, targetID, api.BrowserStateUploadRequest{Cookies: cookies, LocalStorage: result.LocalStorage, SessionStorage: sessions, Domains: syncDomains, Mode: syncMode}) + if useStateImport { + response, status, importErr := client.ProfileStateSetWithResponse(ctx, targetID, stateRequest) if importErr != nil { if created && status >= 400 && status < 500 { _, _ = client.Client().ProfileDeleteWithResponse(ctx, targetID, &api.ProfileDeleteParams{}) @@ -188,6 +188,24 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { return nil } +func buildStateUpload(state browser.State, cookies []api.Cookie, domains []string, mode string, live bool) api.BrowserStateUploadRequest { + if cookies == nil { + cookies = []api.Cookie{} + } + if state.LocalStorage == nil { + state.LocalStorage = map[string]map[string]string{} + } + request := api.BrowserStateUploadRequest{Cookies: cookies, LocalStorage: state.LocalStorage, Domains: domains, Mode: mode} + if live { + if state.SessionStorage == nil { + state.SessionStorage = map[string]map[string]string{} + } + request.SessionStorage = &state.SessionStorage + request.StorageScope = "origins" + } + return request +} + func storageItems(storage map[string]map[string]string) int { count := 0 for _, values := range storage { From 87ff9dc1394639d9d4598d82d7184e8826d9732c Mon Sep 17 00:00:00 2001 From: Andrea Pinto Date: Sun, 18 Oct 2026 02:31:37 +0200 Subject: [PATCH 5/6] Reject incomplete and malformed browser snapshots --- internal/browser/cdp_state.go | 46 ++++++++++++ internal/browser/cdp_state_test.go | 41 ++++++++++- internal/browser/storage.go | 28 ++++++++ internal/browser/storage_test.go | 63 ++++++++++++++++ internal/cmd/profiles_state_sync_test.go | 91 ++++++++++++++++++++++++ internal/cmd/profiles_sync.go | 21 ++++-- 6 files changed, 285 insertions(+), 5 deletions(-) diff --git a/internal/browser/cdp_state.go b/internal/browser/cdp_state.go index 719e241..89ad910 100644 --- a/internal/browser/cdp_state.go +++ b/internal/browser/cdp_state.go @@ -7,8 +7,10 @@ import ( "maps" "net/http" "net/url" + "strconv" "strings" "time" + "unicode/utf8" "github.com/gorilla/websocket" ) @@ -198,6 +200,9 @@ func readTarget(ctx context.Context, endpoint string, domains []string, state *S Local map[string]string `json:"local"` Session map[string]string `json:"session"` } + if !validJSONUnicode(evaluated.Result.Value) { + return fmt.Errorf("live storage contains unsupported Unicode; nothing was uploaded") + } if err := json.Unmarshal([]byte(evaluated.Result.Value), &snapshot); err != nil { return fmt.Errorf("invalid browser storage snapshot") } @@ -216,3 +221,44 @@ func readTarget(ctx context.Context, endpoint string, domains []string, state *S } return nil } + +// encoding/json replaces unpaired UTF-16 surrogates instead of returning an +// error. Reject them in the original JSON strings before that loses information. +func validJSONUnicode(raw string) bool { + if !utf8.ValidString(raw) { + return false + } + inString := false + for i := 0; i < len(raw); i++ { + if raw[i] == '"' { + inString = !inString + } else if inString && raw[i] == '\\' { + if i+1 >= len(raw) { + return false + } + if raw[i+1] != 'u' { + i++ + continue + } + if i+6 > len(raw) { + return false + } + unit, err := strconv.ParseUint(raw[i+2:i+6], 16, 16) + if err != nil || unit >= 0xdc00 && unit <= 0xdfff { + return false + } + if unit >= 0xd800 && unit <= 0xdbff { + if i+12 > len(raw) || raw[i+6:i+8] != `\u` { + return false + } + low, err := strconv.ParseUint(raw[i+8:i+12], 16, 16) + if err != nil || low < 0xdc00 || low > 0xdfff { + return false + } + i += 6 + } + i += 5 + } + } + return true +} diff --git a/internal/browser/cdp_state_test.go b/internal/browser/cdp_state_test.go index 6093cb7..6b329c8 100644 --- a/internal/browser/cdp_state_test.go +++ b/internal/browser/cdp_state_test.go @@ -14,7 +14,7 @@ import ( "github.com/gorilla/websocket" ) -func cdpFixture(t *testing.T, sessions []string, fail bool) *httptest.Server { +func cdpFixture(t *testing.T, sessions []string, fail bool, rawSnapshots ...string) *httptest.Server { t.Helper() var server *httptest.Server upgrader := websocket.Upgrader{CheckOrigin: func(_ *http.Request) bool { return true }} @@ -63,6 +63,9 @@ func cdpFixture(t *testing.T, sessions []string, fail bool) *httptest.Server { t.Error("snapshot must only read storage") } snapshot, _ := json.Marshal(map[string]any{"origin": "https://bsky.app", "local": map[string]string{"BSKY_STORAGE": "synthetic-local-login"}, "session": map[string]string{"tab": sessions[index]}}) + if len(rawSnapshots) > 0 { + snapshot = []byte(rawSnapshots[0]) + } result = map[string]any{"result": map[string]any{"type": "string", "value": string(snapshot)}} default: t.Error("unexpected mutating CDP command", req.Method) @@ -167,3 +170,39 @@ func TestConcurrentLiveSnapshotsRemainIndependent(t *testing.T) { } wait.Wait() } + +func TestLiveStorageUnicodeValidation(t *testing.T) { + for _, raw := range []string{ + `{"local":{"\ud800":"value"}}`, + `{"session":{"key":"\udfff"}}`, + `{"local":{"key":"\ud800x"}}`, + `{"local":{"key":"\ud800\u0041"}}`, + "{\"key\":\"\xff\"}", + } { + if validJSONUnicode(raw) { + t.Error("unsupported live storage string was accepted") + } + } + for _, raw := range []string{ + `{"key":"\ud83d\udd10"}`, + `{"key":"\\ud800"}`, + `{"key":"日本語🔐\u0000\""}`, + } { + if !validJSONUnicode(raw) { + t.Error("valid browser string was rejected") + } + } +} + +func TestLiveStateRefusesUnsupportedUnicodeBeforeUpload(t *testing.T) { + for _, snapshot := range []string{ + `{"origin":"https://bsky.app","local":{"\ud800":"value"},"session":{}}`, + `{"origin":"https://bsky.app","local":{},"session":{"key":"\udfff"}}`, + } { + s := cdpFixture(t, []string{"state"}, false, snapshot) + state, err := ReadCDPState(context.Background(), s.URL, []string{"bsky.app"}) + if err == nil || !strings.Contains(err.Error(), "Unicode") || len(state.Cookies) > 0 || len(state.LocalStorage) > 0 { + t.Fatal("unsupported live strings must abort the entire snapshot", err) + } + } +} diff --git a/internal/browser/storage.go b/internal/browser/storage.go index b79588b..777367b 100644 --- a/internal/browser/storage.go +++ b/internal/browser/storage.go @@ -135,6 +135,11 @@ func (p Profile) readChromiumStorage(domains []string) (map[string]map[string]st return nil, fmt.Errorf("could not read local storage directory") } var copied int64 + type fileVersion struct { + size int64 + modified int64 + } + versions := map[string]fileVersion{} for _, entry := range entries { name := entry.Name() if entry.IsDir() || name == "LOCK" || (name != "CURRENT" && !strings.HasPrefix(name, "MANIFEST-") && !strings.HasSuffix(name, ".log") && !strings.HasSuffix(name, ".ldb") && !strings.HasSuffix(name, ".sst")) { @@ -148,6 +153,7 @@ func (p Profile) readChromiumStorage(domains []string) (map[string]map[string]st return nil, fmt.Errorf("local storage contains an unexpected file") } copied += info.Size() + versions[name] = fileVersion{info.Size(), info.ModTime().UnixNano()} if copied > 512*1024*1024 { return nil, fmt.Errorf("local storage database is too large; use --cdp-url with --domain") } @@ -155,6 +161,28 @@ func (p Profile) readChromiumStorage(domains []string) (map[string]map[string]st return nil, fmt.Errorf("local storage changed while being copied; close the browser and retry") } } + // A multi-file copy is valid only while the source stays unchanged. A WAL + // append, compaction or manifest rotation must fail rather than import a + // potentially incomplete mix of database versions. + after, err := os.ReadDir(source) + if err != nil { + return nil, fmt.Errorf("local storage changed while being copied; close the browser and retry") + } + for _, entry := range after { + name := entry.Name() + if entry.IsDir() || name == "LOCK" || (name != "CURRENT" && !strings.HasPrefix(name, "MANIFEST-") && !strings.HasSuffix(name, ".log") && !strings.HasSuffix(name, ".ldb") && !strings.HasSuffix(name, ".sst")) { + continue + } + info, err := entry.Info() + version, existed := versions[name] + if err != nil || !existed || !info.Mode().IsRegular() || version != (fileVersion{info.Size(), info.ModTime().UnixNano()}) { + return nil, fmt.Errorf("local storage changed while being copied; close the browser and retry") + } + delete(versions, name) + } + if len(versions) != 0 { + return nil, fmt.Errorf("local storage changed while being copied; close the browser and retry") + } db, err := leveldb.OpenFile(tmp, &opt.Options{ReadOnly: true, ErrorIfMissing: true, Strict: opt.StrictAll}) if err != nil { return nil, fmt.Errorf("could not open local storage snapshot; close the browser and retry") diff --git a/internal/browser/storage_test.go b/internal/browser/storage_test.go index 0f20916..f85c3cd 100644 --- a/internal/browser/storage_test.go +++ b/internal/browser/storage_test.go @@ -5,12 +5,16 @@ import ( "encoding/binary" "os" "path/filepath" + "strconv" "strings" "testing" + "time" "unicode/utf16" "github.com/golang/snappy" "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/opt" + "github.com/syndtr/goleveldb/leveldb/util" ) func encodedUTF16(value string) []byte { @@ -231,3 +235,62 @@ func TestFirefoxScopedSyncSkipsUnrelatedCorruptDatabase(t *testing.T) { t.Fatal("selected corrupt storage must fail") } } + +func TestChromiumSnapshotDuringWritesAndCompaction(t *testing.T) { + p, db := localStorageProfile(t) + stop, started := make(chan struct{}), make(chan struct{}) + done := make(chan error, 1) + go func() { + for version := 0; ; version++ { + select { + case <-stop: + done <- nil + return + default: + } + batch := new(leveldb.Batch) + for _, key := range []string{"first", "second"} { + rawKey := append([]byte("_https://bsky.app\x00"), chromeText(key)...) + batch.Put(rawKey, chromeText(strconv.Itoa(version)+strings.Repeat("x", 4096))) + } + if err := db.Write(batch, &opt.WriteOptions{Sync: true}); err != nil { + done <- err + return + } + if version == 0 { + close(started) + } + if version%4 == 0 { + if err := db.CompactRange(util.Range{}); err != nil { + done <- err + return + } + } + time.Sleep(time.Millisecond) + } + }() + defer func() { + close(stop) + if err := <-done; err != nil { + t.Error("fixture writer failed", err) + } + }() + select { + case <-started: + case <-time.After(5 * time.Second): + t.Fatal("fixture writer did not start") + } + for range 30 { + state, err := p.ReadState([]string{"bsky.app"}) + if err != nil { + if !strings.Contains(err.Error(), "local storage") { + t.Fatal("unexpected snapshot error", err) + } + continue + } + values := state.LocalStorage["https://bsky.app"] + if values["first"] == "" || values["first"] != values["second"] { + t.Fatal("concurrent database copy returned an incomplete atomic write") + } + } +} diff --git a/internal/cmd/profiles_state_sync_test.go b/internal/cmd/profiles_state_sync_test.go index a64c9cb..b45ae1b 100644 --- a/internal/cmd/profiles_state_sync_test.go +++ b/internal/cmd/profiles_state_sync_test.go @@ -2,10 +2,14 @@ package cmd import ( "context" + "database/sql" "encoding/json" "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" + "runtime" "strings" "testing" @@ -186,3 +190,90 @@ func TestEmptyLiveSyncNeverCreatesProfile(t *testing.T) { t.Fatal("empty snapshot must fail before accessing the API") } } + +func TestCookieOnlyEndpointCompatibilityAndScope(t *testing.T) { + state := browser.State{LocalStorage: map[string]map[string]string{"https://bsky.app": {}}} + if needsStateImport(state, nil, false) { + t.Fatal("empty storage tables must not require a new server for unscoped disk cookie sync") + } + if !needsStateImport(state, []string{"bsky.app"}, false) { + t.Fatal("scoped cookies require the state API to preserve other sites") + } + if !needsStateImport(state, nil, true) { + t.Fatal("live snapshots must replace captured origins without clearing closed ones") + } + state.LocalStorage["https://bsky.app"]["token"] = "synthetic" + if !needsStateImport(state, nil, false) { + t.Fatal("storage values must never be silently dropped for cookie API compatibility") + } +} + +func TestLegacyCookieSyncRefusesRedirectsAndRedactsErrors(t *testing.T) { + if !browser.SupportedPlatform() { + t.Skip("disk browser sync is not supported on this OS") + } + for _, status := range []int{200, 307, 308, 422} { + t.Run(fmt.Sprint(status), func(t *testing.T) { + env := testutil.SetupTestEnv(t) + env.SetEnv("HOME", env.TempDir) + env.SetEnv("NOTTE_API_KEY", "test-key") + configureLiveSyncTest(t, "") + profileID, syncDomains, syncBrowser, syncChromeProfile = profileIDTest, nil, "firefox", "Synthetic" + root := filepath.Join(env.TempDir, ".mozilla", "firefox") + if runtime.GOOS == "darwin" { + root = filepath.Join(env.TempDir, "Library", "Application Support", "Firefox") + } + dir := filepath.Join(root, "synthetic") + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "profiles.ini"), []byte("[Profile0]\nName=Synthetic\nIsRelative=1\nPath=synthetic\n"), 0o600); err != nil { + t.Fatal(err) + } + db, err := sql.Open("sqlite", filepath.Join(dir, "cookies.sqlite")) + if err != nil { + t.Fatal(err) + } + for _, statement := range []string{ + `CREATE TABLE moz_cookies(host TEXT, name TEXT, value TEXT, path TEXT, expiry INTEGER, isSecure INTEGER, isHttpOnly INTEGER, sameSite INTEGER, originAttributes TEXT)`, + `INSERT INTO moz_cookies VALUES('bsky.app','login','synthetic-private-token','/',1893456000,1,1,1,'')`, + } { + if _, err := db.Exec(statement); err != nil { + t.Fatal(err) + } + } + if err := db.Close(); err != nil { + t.Fatal(err) + } + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Error("browser credentials were forwarded on redirect") + })) + defer target.Close() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasSuffix(r.URL.Path, "/cookies") { + t.Error("unscoped cookie-only sync did not use the compatible endpoint", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Location", target.URL) + w.WriteHeader(status) + if status == 200 { + _, _ = w.Write([]byte(`{"success":true,"cookies_count":1,"mode":"replace","message":"imported"}`)) + } else { + _, _ = w.Write([]byte(`{"detail":"synthetic-private-token"}`)) + } + })) + defer server.Close() + env.SetEnv("NOTTE_API_URL", server.URL) + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + var runErr error + stdout, stderr := testutil.CaptureOutput(func() { runErr = runProfilesSync(cmd, nil) }) + if (runErr == nil) != (status == 200) { + t.Fatal("cookie import reported an incorrect result", runErr) + } + if strings.Contains(stdout+stderr+fmt.Sprint(runErr), "synthetic-private-token") { + t.Fatal("cookie values leaked into output") + } + }) + } +} diff --git a/internal/cmd/profiles_sync.go b/internal/cmd/profiles_sync.go index 36fc286..6b34f9e 100644 --- a/internal/cmd/profiles_sync.go +++ b/internal/cmd/profiles_sync.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "net/http" "os" "sort" "strconv" @@ -93,7 +94,7 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { return fmt.Errorf("no cookies or web storage found for the selected sites") } cookies := toAPICookies(result.Cookies) - useStateImport := len(result.LocalStorage) > 0 || len(result.SessionStorage) > 0 || len(syncDomains) > 0 || syncCDPURL != "" + useStateImport := needsStateImport(result, syncDomains, syncCDPURL != "") stateRequest := buildStateUpload(result, cookies, syncDomains, syncMode, syncCDPURL != "") if useStateImport { if err := stateRequest.Validate(); err != nil { @@ -129,6 +130,8 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { if err != nil { return err } + // Cookie-only uploads also contain credentials and must refuse redirects. + client.HTTPClient().CheckRedirect = func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse } targetID := profileID created := false @@ -165,10 +168,16 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { resp, err := client.Client().ProfileCookiesSetWithResponse(ctx, targetID, &api.ProfileCookiesSetParams{}, body) if err != nil { - return fmt.Errorf("API request failed: %w", err) + return fmt.Errorf("cookie import could not be confirmed; inspect profile %s before retrying", targetID) } - if err := HandleAPIResponse(resp.HTTPResponse, resp.Body); err != nil { - return err + if resp.StatusCode() != http.StatusOK { + if created && resp.StatusCode() >= 400 && resp.StatusCode() < 500 { + _, _ = client.Client().ProfileDeleteWithResponse(ctx, targetID, &api.ProfileDeleteParams{}) + } + return fmt.Errorf("cookie import failed (HTTP %d); browser values were not printed", resp.StatusCode()) + } + if resp.JSON200 == nil || !resp.JSON200.Success { + return fmt.Errorf("cookie import returned an invalid confirmation for profile %s", targetID) } if IsJSONOutput() { @@ -188,6 +197,10 @@ func runProfilesSync(cmd *cobra.Command, args []string) error { return nil } +func needsStateImport(state browser.State, domains []string, live bool) bool { + return storageItems(state.LocalStorage) > 0 || storageItems(state.SessionStorage) > 0 || len(domains) > 0 || live +} + func buildStateUpload(state browser.State, cookies []api.Cookie, domains []string, mode string, live bool) api.BrowserStateUploadRequest { if cookies == nil { cookies = []api.Cookie{} From a4897e4bf41fcc2edd5e7f08bc45cd2bec658b8e Mon Sep 17 00:00:00 2001 From: Andrea Pinto Date: Mon, 26 Oct 2026 17:01:06 -0700 Subject: [PATCH 6/6] Use a vault-backed session in the payment integration test --- tests/integration/payments_test.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/integration/payments_test.go b/tests/integration/payments_test.go index 443b480..f535229 100644 --- a/tests/integration/payments_test.go +++ b/tests/integration/payments_test.go @@ -38,7 +38,19 @@ func TestPaymentSandboxLifecycle(t *testing.T) { t.Fatalf("payment preflight failed: status=%d error=%s", apiError.StatusCode, apiError.Error) } - started := runCLI(t, "sessions", "start", "--proxy=false", "--no-solve-captchas", "--idle-timeout-minutes", "5", "--max-duration-minutes", "10") + // Payments require the session's worker connection and card vault. An + // explicit empty vault selects that path instead of a workerless session. + createdVault := runCLI(t, "vaults", "create", "--name", "CLI sandbox payment test") + requireSuccess(t, createdVault) + var vault struct { + ID string `json:"vault_id"` + } + if err := json.Unmarshal([]byte(createdVault.Stdout), &vault); err != nil || vault.ID == "" { + t.Fatal("vault create returned no vault ID") + } + t.Cleanup(func() { cleanupVault(t, vault.ID) }) + + started := runCLI(t, "sessions", "start", "--vault-id", vault.ID, "--proxy=false", "--no-solve-captchas", "--idle-timeout-minutes", "5", "--max-duration-minutes", "10") requireSuccess(t, started) var session struct { ID string `json:"session_id"` @@ -116,7 +128,7 @@ func TestPaymentSandboxLifecycle(t *testing.T) { denied := runCLI(t, args...) requireFailure(t, denied) if !strings.Contains(denied.Stderr, "wallet_not_connected") || !strings.Contains(denied.Stderr, "payment connect") { - t.Fatal("missing connect-first guidance") + t.Fatalf("missing connect-first guidance: %s", denied.Stderr) } return // Manual wallet authorization is intentionally not automated in CI. }