diff --git a/src/libs/AssemblyAI/Generated/AssemblyAI.AutoSDKPager.g.cs b/src/libs/AssemblyAI/Generated/AssemblyAI.AutoSDKPager.g.cs index 5f364d5..00bb518 100644 --- a/src/libs/AssemblyAI/Generated/AssemblyAI.AutoSDKPager.g.cs +++ b/src/libs/AssemblyAI/Generated/AssemblyAI.AutoSDKPager.g.cs @@ -10,6 +10,85 @@ namespace AssemblyAI /// public static class AutoSDKPager { + /// Copies request options for one generated next-URL page request. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::AssemblyAI.AutoSDKRequestOptions CreatePageRequestOptions( + global::AssemblyAI.AutoSDKRequestOptions? source, + string? nextUrl) + { + var clone = new global::AssemblyAI.AutoSDKRequestOptions(); + if (source is not null) + { + foreach (var header in source.Headers) + { + clone.Headers[header.Key] = header.Value; + } + foreach (var query in source.QueryParameters) + { + clone.QueryParameters[query.Key] = query.Value; + } + clone.Timeout = source.Timeout; + clone.Retry = source.Retry; + clone.ReadResponseAsString = source.ReadResponseAsString; + clone.Authorizations = source.Authorizations; + } + clone.PaginationUrl = nextUrl; + return clone; + } + + /// Returns a validated next-page URL, or null for an ordinary request. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static string? ResolvePageUrl( + string path, + global::AssemblyAI.AutoSDKRequestOptions? requestOptions, + global::System.Uri? baseAddress) + { + var nextUrl = requestOptions?.PaginationUrl; + if (string.IsNullOrWhiteSpace(nextUrl)) + { + return null; + } + + if (baseAddress is null && + global::System.Uri.TryCreate(path, global::System.UriKind.Absolute, out var pathUri) && + (pathUri.Scheme == global::System.Uri.UriSchemeHttp || pathUri.Scheme == global::System.Uri.UriSchemeHttps)) + { + baseAddress = pathUri; + } + if (baseAddress is not null) + { + var resolved = new global::System.Uri(baseAddress, nextUrl!); + EnsureSameOrigin(resolved.AbsoluteUri, baseAddress); + return resolved.AbsoluteUri; + } + if (global::System.Uri.TryCreate(nextUrl, global::System.UriKind.Absolute, out _)) + { + throw new global::System.InvalidOperationException( + "Cannot follow an absolute next-page URL without a known base address."); + } + + return nextUrl; + } + + /// Uses the HTTP client's origin, or the operation server when it has one. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::System.Uri? GetPaginationBaseAddress( + global::System.Uri? clientBaseAddress, + string operationBaseUrl) + { + if (clientBaseAddress is not null) + { + return clientBaseAddress; + } + + return global::System.Uri.TryCreate( + operationBaseUrl, + global::System.UriKind.Absolute, + out var serverBaseAddress) + ? serverBaseAddress + : null; + } + /// /// Paginates through offset/page-number-style endpoints. Calls /// repeatedly with an incrementing page number until the page returns no items, or the @@ -74,7 +153,7 @@ public static class AutoSDKPager /// /// Paginates through cursor/next-page-token-style endpoints. Calls /// with the cursor returned by the previous page until - /// returns null or whitespace. + /// returns null or whitespace, or a cursor repeats. /// /// /// @@ -96,9 +175,14 @@ public static class AutoSDKPager extractNextCursor = extractNextCursor ?? throw new global::System.ArgumentNullException(nameof(extractNextCursor)); var cursor = initialCursor; + var seenCursors = new global::System.Collections.Generic.HashSet(global::System.StringComparer.Ordinal); while (true) { cancellationToken.ThrowIfCancellationRequested(); + if (!string.IsNullOrWhiteSpace(cursor) && !seenCursors.Add(cursor!)) + { + yield break; + } var response = await fetchPage(cursor, cancellationToken).ConfigureAwait(false); if (response is null) @@ -156,9 +240,14 @@ public static class AutoSDKPager } var url = initialUrl; + var seenUrls = new global::System.Collections.Generic.HashSet(global::System.StringComparer.Ordinal); while (true) { cancellationToken.ThrowIfCancellationRequested(); + if (!string.IsNullOrWhiteSpace(url) && !seenUrls.Add(url!)) + { + yield break; + } var response = await fetchPage(url, cancellationToken).ConfigureAwait(false); if (response is null) @@ -224,7 +313,7 @@ public static class AutoSDKPager /// Paginates through APIs that embed an absolute next URL in the response body /// (Firecrawl, Slack, Linear, Notion, HubSpot, etc.). Calls /// with the URL returned by from the previous page - /// until the extractor returns null/whitespace. + /// until the extractor returns null/whitespace or a URL repeats. /// /// Security: when is provided, every next-URL /// origin must match it before the next fetch runs. An unchecked walker would let a @@ -254,12 +343,17 @@ public static class AutoSDKPager extractNextUrl = extractNextUrl ?? throw new global::System.ArgumentNullException(nameof(extractNextUrl)); var url = initialUrl; + var seenUrls = new global::System.Collections.Generic.HashSet(global::System.StringComparer.Ordinal); while (true) { cancellationToken.ThrowIfCancellationRequested(); if (!string.IsNullOrWhiteSpace(url)) { + if (!seenUrls.Add(url!)) + { + yield break; + } EnsureSameOrigin(url!, baseAddress); } @@ -291,7 +385,8 @@ public static class AutoSDKPager /// /// Validates that shares an origin with . /// Throws when they differ so an embedded foreign URL can't redirect the SDK (and the - /// caller's Authorization header) to a hostile host. When + /// caller's Authorization header) to a hostile host. Relative URLs are resolved + /// before comparison, including protocol-relative URLs. When /// is null, validation is skipped — callers should pass HttpClient.BaseAddress. /// public static void EnsureSameOrigin(string nextUrl, global::System.Uri? baseAddress) @@ -301,11 +396,7 @@ public static void EnsureSameOrigin(string nextUrl, global::System.Uri? baseAddr return; } - if (!global::System.Uri.TryCreate(nextUrl, global::System.UriKind.Absolute, out var absolute)) - { - // Relative URLs are safe — they're resolved against the client's BaseAddress on send. - return; - } + var absolute = new global::System.Uri(baseAddress, nextUrl); if (!string.Equals(absolute.Scheme, baseAddress.Scheme, global::System.StringComparison.OrdinalIgnoreCase) || !string.Equals(absolute.Host, baseAddress.Host, global::System.StringComparison.OrdinalIgnoreCase) || diff --git a/src/libs/AssemblyAI/Generated/AssemblyAI.OptionsSupport.g.cs b/src/libs/AssemblyAI/Generated/AssemblyAI.OptionsSupport.g.cs index 3261d49..1fd16c4 100644 --- a/src/libs/AssemblyAI/Generated/AssemblyAI.OptionsSupport.g.cs +++ b/src/libs/AssemblyAI/Generated/AssemblyAI.OptionsSupport.g.cs @@ -308,6 +308,9 @@ private static void ApplyAuthorization( /// public sealed class AutoSDKRequestOptions { + // Set only by AutoSDKPager on a generated next-URL paging call. + internal string? PaginationUrl { get; set; } + /// /// Additional headers applied after generated and client-level headers. ///