Skip to content

fix(oauth2): port Node SDK PR #11 — refresh_token, forceRefresh, 120s buffer, refreshEndpoint, re-entrance guard#94

Merged
Theosakamg merged 13 commits into
mainfrom
fix/oauth2-refresh-token
Jun 5, 2026
Merged

fix(oauth2): port Node SDK PR #11 — refresh_token, forceRefresh, 120s buffer, refreshEndpoint, re-entrance guard#94
Theosakamg merged 13 commits into
mainfrom
fix/oauth2-refresh-token

Conversation

@Theosakamg

Copy link
Copy Markdown
Contributor

Context

Ports the 5 OAuth2 fixes from upsun-sdk-node PR #11 to the PHP SDK. All fixes are applied to the Mustache templates (source of truth for OpenAPI Generator) and to the generated files directly.


Fixes

FIX 1 — 401 retry middleware calls forceRefresh() instead of exchangeCodeForToken()

AbstractApi::sendAuthenticatedRequest() now intercepts HTTP 401 responses, calls OAuthProvider::forceRefresh() (which prefers the refresh_token grant), and retries the request once.

RFC 6750 §3.1 + RFC 6749 Fig. 2, steps F→G→H

FIX 2 — refreshAccessToken() sends Authorization: Basic

The new refreshAccessToken() method sends Authorization: Basic base64("platform-api-user:") as required for confidential clients.

RFC 6749 §3.2.1

FIX 3 — Proactive refresh buffer raised from 60 s → 120 s

Mitigates clock skew between client and auth server.

FIX 4 — Configurable refreshEndpoint (distinct from tokenEndpoint)

OAuthProvider accepts an optional refreshEndpoint constructor parameter (defaults to tokenEndpoint). UpsunClient passes auth_url + "/" + refresh_endpoint from UpsunConfig.

FIX 5 — Re-entrance guard (thundering-herd protection)

$acquiringToken: bool flag prevents recursive acquisition in PHP Fiber contexts. Multi-process FPM thundering-herd would require an external lock (Redis/APCu) — out of scope.


New methods

  • forceRefresh() — public, clears accessToken/tokenExpiry without touching refreshToken or acquiringToken, then delegates to ensureValidToken()
  • refreshAccessToken() — private, grant_type=refresh_token via refreshEndpoint
  • doAcquireToken() — private, prefers refresh_token grant, falls back to api_token

Tests

9 new unit tests added (567 total, 3395 assertions — all green):

Test Fix
testRefreshAccessTokenSendsBasicAuthHeader FIX 2
testRefreshAccessTokenUsesGrantTypeRefreshToken FIX 2
testEnsureValidTokenPrefersRefreshTokenGrant FIX 1 + 3
testFallbackToApiTokenIfRefreshFails FIX 1
testForceRefreshTriggersAcquisitionEvenWithValidToken FIX 1
testForceRefreshPrefersRefreshTokenIfAvailable FIX 1
testConcurrentForceRefreshCallsResultInSingleRequest FIX 5
testRefreshEndpointUsedForRefreshTokenGrant FIX 4
testBuffer120SecondsTriggersProactiveRefresh FIX 3

Similarity with Node SDK

~90% functional parity. The only intentional divergence is FIX 5: Node uses a shared Promise (pendingToken) to deduplicate truly concurrent async callers; PHP uses a boolean re-entrance guard (PHP-FPM is single-threaded per process).


Files changed

File Note
templates/php/oauth_provider.mustache Source of truth — all OAuth2 fixes
templates/php/abstract_api.mustache Source of truth — 401 retry
src/Core/OAuthProvider.php Generated output (kept in sync)
src/Api/AbstractApi.php Generated output (kept in sync)
src/UpsunClient.php Wires refreshEndpoint to OAuthProvider
tests/Core/OAuthProviderTest.php +9 tests

… buffer, refreshEndpoint, re-entrance guard

FIX 1 — 401 retry middleware calls forceRefresh() instead of exchangeCodeForToken()
        AbstractApi::sendAuthenticatedRequest() intercepts 401, calls
        OAuthProvider::forceRefresh() and retries once (RFC 6750 §3.1).

FIX 2 — refreshAccessToken() sends Authorization: Basic header
        Required for confidential clients per RFC 6749 §3.2.1.

FIX 3 — Proactive refresh buffer raised from 60 s to 120 s
        Mitigates clock skew between client and auth server.

FIX 4 — Configurable refreshEndpoint (distinct from tokenEndpoint)
        OAuthProvider accepts an optional refreshEndpoint parameter (default =
        tokenEndpoint). UpsunClient passes auth_url + refresh_endpoint.

FIX 5 — Re-entrance guard (thundering-herd protection)
        acquiringToken bool prevents recursive acquisition in PHP Fiber
        contexts. Multi-process FPM protection requires external lock (out of scope).

Also adds:
- doAcquireToken(): prefers refresh_token grant, falls back to api_token
- forceRefresh(): clears access token without touching refreshToken or acquiringToken
- storeTokenData() now persists refresh_token and token_type from response
- 9 new unit tests covering all 5 fixes (567 tests, 3395 assertions — all green)
- Minor CI workflow label fixes and composer clean script
@platformsh-devrel

platformsh-devrel commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

🧾 API Coverage Report

Last updated: 3e5bb53 • Run #235

Metric Value
Total API methods 272
Called from facades 191
Unmapped API methods 81
Invalid Facade API calls 0
Coverage 70.22%
📋 Full JSON report
{
  "total_api_methods": 272,
  "called_methods": 191,
  "unmapped_methods": [
    "Upsun\\Api\\AutoscalingApi::patchAutoscalerSettings",
    "Upsun\\Api\\OrganizationProjectsApi::createOrgProject",
    "Upsun\\Api\\OrganizationProjectsApi::deleteOrgProject",
    "Upsun\\Api\\OrganizationProjectsApi::getOrgProject",
    "Upsun\\Api\\OrganizationProjectsApi::queryProjectCarbon",
    "Upsun\\Api\\OrganizationProjectsApi::streamOrgProjectProvisioning",
    "Upsun\\Api\\ReferencesApi::listReferencedOrgs",
    "Upsun\\Api\\ReferencesApi::listReferencedProjects",
    "Upsun\\Api\\ReferencesApi::listReferencedRegions",
    "Upsun\\Api\\ReferencesApi::listReferencedTeams",
    "Upsun\\Api\\ReferencesApi::listReferencedUsers",
    "Upsun\\Api\\BlackfireProfilingApi::blackfireProfileGraph",
    "Upsun\\Api\\BlackfireProfilingApi::blackfireProfileProfile",
    "Upsun\\Api\\BlackfireProfilingApi::blackfireProfileSubprofiles",
    "Upsun\\Api\\BlackfireProfilingApi::blackfireProfileTimeline",
    "Upsun\\Api\\BlackfireProfilingApi::blackfireProfilesList",
    "Upsun\\Api\\BlackfireProfilingApi::blackfireProfilesRecommendations",
    "Upsun\\Api\\ResourcesApi::resourcesByService",
    "Upsun\\Api\\ResourcesApi::resourcesOverview",
    "Upsun\\Api\\ResourcesApi::resourcesSummary",
    "Upsun\\Api\\SbomApi::getProjectsEnvironmentsDeploymentsSboms",
    "Upsun\\Api\\SbomApi::listProjectsEnvironmentsDeploymentsSboms",
    "Upsun\\Api\\AlertsApi::getUsageAlerts",
    "Upsun\\Api\\AlertsApi::updateUsageAlerts",
    "Upsun\\Api\\DomainClaimApi::createProjectsDomainClaims",
    "Upsun\\Api\\DomainClaimApi::deleteProjectsDomainClaims",
    "Upsun\\Api\\DomainClaimApi::getProjectsDomainClaims",
    "Upsun\\Api\\DomainClaimApi::listProjectsDomainClaims",
    "Upsun\\Api\\DomainClaimApi::updateProjectsDomainClaims",
    "Upsun\\Api\\GrantsApi::getAccessDocument",
    "Upsun\\Api\\SubscriptionsApi::canAffordSubscription",
    "Upsun\\Api\\SubscriptionsApi::canUpdateSubscription",
    "Upsun\\Api\\SubscriptionsApi::getSubscriptionUsageAlerts",
    "Upsun\\Api\\SubscriptionsApi::listSubscriptionAddons",
    "Upsun\\Api\\SubscriptionsApi::updateOrgSubscription",
    "Upsun\\Api\\SubscriptionsApi::updateSubscriptionUsageAlerts",
    "Upsun\\Api\\DiscountsApi::getDiscount",
    "Upsun\\Api\\DiscountsApi::getTypeAllowance",
    "Upsun\\Api\\DiscountsApi::listOrgDiscounts",
    "Upsun\\Api\\OrganizationManagementApi::estimateOrg",
    "Upsun\\Api\\OrganizationManagementApi::getOrgBillingAlertConfig",
    "Upsun\\Api\\OrganizationManagementApi::getOrgPrepaymentInfo",
    "Upsun\\Api\\OrganizationManagementApi::listOrgPrepaymentTransactions",
    "Upsun\\Api\\OrganizationManagementApi::updateOrgBillingAlertConfig",
    "Upsun\\Api\\RegistryCredentialApi::createProjectsOciRegistries",
    "Upsun\\Api\\RegistryCredentialApi::deleteProjectsOciRegistries",
    "Upsun\\Api\\RegistryCredentialApi::getProjectsOciRegistries",
    "Upsun\\Api\\RegistryCredentialApi::listProjectsOciRegistries",
    "Upsun\\Api\\RegistryCredentialApi::updateProjectsOciRegistries",
    "Upsun\\Api\\ContinuousProfilingApi::getApplicationFilter",
    "Upsun\\Api\\ContinuousProfilingApi::getApplicationMerge",
    "Upsun\\Api\\ContinuousProfilingApi::getApplicationTimeline",
    "Upsun\\Api\\ContinuousProfilingApi::listApplications",
    "Upsun\\Api\\SystemInformationApi::actionProjectsSystemRestart",
    "Upsun\\Api\\ProjectsApi::listOrgProjectHistory",
    "Upsun\\Api\\EnvironmentApi::deployEnvironment",
    "Upsun\\Api\\EnvironmentApi::maintenanceRedeployEnvironment",
    "Upsun\\Api\\DiffApi::listProjectsGitDiffs",
    "Upsun\\Api\\DeploymentTargetApi::createProjectsDeployments",
    "Upsun\\Api\\DeploymentTargetApi::deleteProjectsDeployments",
    "Upsun\\Api\\DeploymentTargetApi::getProjectsDeployments",
    "Upsun\\Api\\DeploymentTargetApi::listProjectsDeployments",
    "Upsun\\Api\\DeploymentTargetApi::updateProjectsDeployments",
    "Upsun\\Api\\CertManagementApi::getProjectsProvisioners",
    "Upsun\\Api\\CertManagementApi::listProjectsProvisioners",
    "Upsun\\Api\\CertManagementApi::updateProjectsProvisioners",
    "Upsun\\Api\\DefaultApi::queryOrganiationCarbon",
    "Upsun\\Api\\UserProfilesApi::createProfilePicture",
    "Upsun\\Api\\HttpTrafficApi::httpMetricsTimelineIps",
    "Upsun\\Api\\HttpTrafficApi::httpMetricsTimelineUrls",
    "Upsun\\Api\\HttpTrafficApi::httpMetricsTimelineUserAgents",
    "Upsun\\Api\\EntrypointApi::observabilityEntrypoint",
    "Upsun\\Api\\BlackfireMonitoringApi::blackfirePhpServerCaches",
    "Upsun\\Api\\BlackfireMonitoringApi::blackfireServerGlobal",
    "Upsun\\Api\\BlackfireMonitoringApi::blackfireServerTopSpans",
    "Upsun\\Api\\BlackfireMonitoringApi::blackfireServerTransactionsBreakdown",
    "Upsun\\Api\\UsersApi::getCurrentUserDeprecated",
    "Upsun\\Api\\ProjectApi::maintenanceRedeployProject",
    "Upsun\\Api\\TaskApi::getProjectsEnvironmentsTasks",
    "Upsun\\Api\\TaskApi::listProjectsEnvironmentsTasks",
    "Upsun\\Api\\TaskApi::runTask"
  ],
  "invalid_facade_calls": [],
  "coverage_percentage": 70.22
}

@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.80328% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/Core/OAuthProvider.php 91.07% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


doAcquireToken() already guards $this->refreshToken before calling
refreshAccessToken(), making the inner check dead code (never reachable).
Removing it achieves 100% line + method coverage on OAuthProvider.

Also syncs template (oauth_provider.mustache) and commits phpunit.xml
coverage-scope update (added src/Api/AbstractApi.php).
@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


…double-retry

Change 1 — Bearer-only mode + \Closure tokenProvider in AbstractApi
- AbstractApi: replace OAuthProvider $oauthProvider with \Closure $tokenProvider
  - getAuthorizationHeader() → ($this->tokenProvider)()
  - refreshToken() → ($this->tokenProvider)()
- All ~62 concrete API files: sed OAuthProvider → \Closure tokenProvider
- api.mustache + abstract_api.mustache: updated to match
- UpsunClient: $auth nullable (?OAuthProvider), only created when apiToken !== ''
  - new setBearerToken(string $token): void
  - getToken() three-way: auth → bearerToken → throw RuntimeException
  - $tokenProvider closure passed to all concrete APIs instead of $this->auth

Change 2 — Fiber-based thundering-herd guard in OAuthProvider::ensureValidToken()
- Replace bool $acquiringToken with ?\Fiber $acquiringFiber
- Concurrent Fiber callers suspend (via \Fiber::getCurrent()?->suspend()) until the
  first acquisition completes, then return without a second HTTP call
- In sync (FPM) contexts \Fiber::getCurrent() is null, so acquiringFiber is always
  null and the guard is a no-op — identical behaviour to the old bool flag

Change 3 — $_retried guard in sendAuthenticatedRequest() prevents double forceRefresh
- Add bool $_retried = false param; on 401 call tokenProvider(true) then recurse
  with $_retried=true so a second 401 goes straight to ApiException without
  a redundant force-refresh call

Tests:
- AbstractApiTest: rewritten to use closure tracking (tokenCallCount/forceRefreshCount)
  instead of OAuthProvider mock; 401-retry $_retried guard verified
- UpsunClientTest: removed testGetTokenReturnsApiToken (now HTTP-backed); added
  testGetTokenDelegatesToOAuthProvider, testGetTokenReturnsBearerToken,
  testGetTokenThrowsWhenNoAuthMethodSet
- OAuthProviderTest: replaced reflection-based acquiringToken test with proper
  Fiber concurrency test (testFiberGuardPreventsDoubleAcquisitionUnderConcurrency)
- BaseTestCase + all ~25 TaskTests: OAuthProvider mock → \Closure tokenProvider

Result: 584/584 tests pass, lint clean
@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


…uisition

Replaces bare Closure with a proper TokenProvider interface, aligning naming
with the Node SDK's `type TokenProvider = (force?: boolean) => string`.

- src/Core/TokenProvider.php: new interface with __invoke(bool $force=false): string
- OAuthProvider: implements TokenProvider, adds __invoke() covering force-refresh path
- AbstractApi: TokenProvider type hint replacing Closure
- UpsunClient: $tokenProvider declared as named variable before $taskParams array
  (facade does not implement the interface — clean separation of concerns)
- All 62 concrete API files + mustache templates updated
- All tests updated: anonymous class implements TokenProvider (Closure != interface in PHP)
- 25 task test files: multi-line anonymous class format (PHPCS compliant)
…d file

Alphabetical use-statement order (matching PHPCS rule) was broken after
introducing TokenProvider — `use {{invokerPackage}}\Core\TokenProvider;`
was sitting between JsonException and Http\ imports.
Now matches the order produced by phpcbf on AbstractApi.php.
@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


…code

fix:all (phpcbf + rector + php-cs-fixer) applied:
- OAuthProvider: remove redundant `use Upsun\Core\TokenProvider;` (same namespace)
- 25 task test files: FQNS `\Upsun\Core\TokenProvider` → `use` import + short name

oauth_provider.mustache synced with OAuthProvider.php:
- use Fiber; import (was \Fiber FQNS)
- class OAuthProvider implements TokenProvider
- short Fiber name throughout (not \Fiber)
- __invoke(bool $force = false): string method added
@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


1 similar comment
@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Ports multiple OAuth2/token-lifecycle fixes from the Node SDK into the PHP SDK, introducing a TokenProvider abstraction so generated API clients can support OAuth2 refresh semantics, 401 retry, and bearer-only authentication.

Changes:

  • Introduces Core\TokenProvider and refactors generated API clients + AbstractApi to depend on it (enabling force-refresh on 401 and bearer-only mode).
  • Enhances OAuthProvider with refresh-token grant support (Basic auth), configurable refresh endpoint, 120s proactive refresh buffer, and a Fiber re-entrance guard.
  • Adds/updates unit tests for the new token behaviors and updates CI workflows / coverage configuration.

Reviewed changes

Copilot reviewed 104 out of 105 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Api/AbstractApi.php Switches auth dependency to TokenProvider and adds 401 retry logic (but currently has a constructor bug: undefined $plugins/$uriFactory).
templates/php/abstract_api.mustache Template source for the AbstractApi changes (same constructor bug as generated output).
src/Core/OAuthProvider.php Implements TokenProvider, adds refresh-token flow, 120s buffer, refresh endpoint, and Fiber guard (but currently calls Fiber suspend incorrectly).
templates/php/oauth_provider.mustache Template source for OAuthProvider changes (same Fiber suspend issue).
src/Core/TokenProvider.php New interface to supply Authorization header values and support force-refresh.
src/UpsunClient.php Wires refreshEndpoint, supports bearer-only auth, and provides a TokenProvider adapter for generated APIs.
src/Api/AddOnsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/AlertsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ApiConfiguration.php Docstring capitalization update (ApiConfiguration).
src/Api/ApiTokensApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ApiTokensApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/AutoscalingApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/BlackfireMonitoringApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/BlackfireProfilingApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/CertManagementApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ConnectionsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ContinuousProfilingApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/DefaultApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/DeploymentApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/DeploymentTargetApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/DiffApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/DiscountsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/DomainClaimApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/DomainManagementApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/EntrypointApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/EnvironmentActivityApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/EnvironmentApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/EnvironmentBackupsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/EnvironmentTypeApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/EnvironmentVariablesApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/GrantsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/HttpTrafficApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/InvoicesApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/MfaApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/OrdersApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/OrganizationInvitationsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/OrganizationManagementApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/OrganizationMembersApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/OrganizationProjectsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/OrganizationsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/PhoneNumberApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ProfilesApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ProjectActivityApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ProjectApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ProjectInvitationsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ProjectsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ProjectSettingsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ProjectVariablesApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/RecordsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ReferencesApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/RegionsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/RegistryCredentialApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/RepositoryApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ResourcesApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/RoutingApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/RuntimeOperationsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/SbomApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/SourceOperationsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/SshKeysApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/SubscriptionsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/SupportApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/SystemInformationApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/TaskApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/TeamAccessApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/TeamsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/ThirdPartyIntegrationsApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/UserAccessApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/UserProfilesApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/UsersApi.php Updates generated API constructor signature to accept TokenProvider.
src/Api/VouchersApi.php Updates generated API constructor signature to accept TokenProvider.
templates/php/libraries/psr-18/api.mustache Updates API generation template to accept TokenProvider and ApiConfiguration naming.
templates/php/Configuration.mustache Docstring capitalization update (ApiConfiguration).
tests/Api/AbstractApiTest.php New tests covering sendAuthenticatedRequest() 401 retry behavior via TokenProvider(force=true).
tests/Core/OAuthProviderTest.php Adds tests for refresh-token grant behavior, refresh endpoint, buffer change, and Fiber guard (contains a Fiber API misuse).
tests/UpsunClientTest.php Updates/extends auth token retrieval tests for OAuth + bearer-only mode.
tests/Core/Tasks/BaseTestCase.php Refactors task test harness to use TokenProvider instead of OAuthProvider.
tests/Core/Tasks/ActivitiesTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/ApplicationsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/BackupsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/CertificatesTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/DomainsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/EnvironmentsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/IntegrationsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/InvitationsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/MountsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/OperationsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/OrganizationsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/ProjectsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/RegionsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/RepositoriesTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/ResourcesTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/RoutesTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/ServicesTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/SourceOperationsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/SshTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/SupportTicketsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/TeamsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/UsersInvitationsTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/UsersTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/VariablesTaskTest.php Refactors to use TokenProvider in setup.
tests/Core/Tasks/WorkersTaskTest.php Refactors to use TokenProvider in setup.
phpunit.xml Extends coverage inclusion to include src/Api/AbstractApi.php.
docs/php-alignment-node-prompt.md Adds an internal alignment/implementation guide for porting Node SDK behavior.
.github/workflows/develop.yml Workflow step naming tweak for clarity.
.github/workflows/publish.yml Uses composer run install:ci for consistency with other workflows.
Comments suppressed due to low confidence (2)

src/Api/AbstractApi.php:49

  • AbstractApi::__construct() references $plugins and $uriFactory, but they are not defined in the constructor parameters anymore. This will trigger undefined variable errors and prevents customizing the plugin chain / URI factory as intended.
    public function __construct(
        private readonly TokenProvider $tokenProvider,
        private ClientInterface $httpClient,
        private readonly RequestFactoryInterface $requestFactory,
        private readonly string $baseUri,
        ?StreamFactoryInterface $streamFactory = null,
    ) {
        $plugins = $plugins ?? [

templates/php/abstract_api.mustache:57

  • The AbstractApi template uses $plugins and $uriFactory inside __construct(), but they are not declared as parameters. The generated AbstractApi.php will therefore reference undefined variables.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Core/OAuthProvider.php Outdated
Comment thread templates/php/oauth_provider.mustache Outdated
->method('sendRequest')
->willReturnCallback(function () use (&$httpCallCount, $tokenResponse) {
$httpCallCount++;
\Fiber::getCurrent()?->suspend(); // simulate async I/O suspending the Fiber
…lsafe instance call

Fiber::suspend() is a static method. Calling it via a nullsafe instance
reference ($fiber?->suspend()) is technically valid but a PHP anti-pattern
(calling static methods on instances). The correct form is:

  if (Fiber::getCurrent() !== null) { Fiber::suspend(); }

This is explicit about the guard intent and avoids potential deprecation
warnings from static-via-instance call patterns.
@upsun-bot

Copy link
Copy Markdown

Upsun SDK checker report

Display raw output

════════════════════════════════════════════════════════════════════════════════
  SDK Signature Comparison Report
════════════════════════════════════════════════════════════════════════════════


📦 Class: DomainsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: add()
       node: (projectId: string, domain: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: EnvironmentsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: init()
       node: (projectId: string, environmentId: string, profile: string, repository: string, files: FilesInner[])
       php: (projectId: string, environmentId: string, profile: string, repository: string, fileMode: string, filePath: string, fileContents: string)

     Method: addDomain()
       node: (projectId: string, environmentId: string, domainName: string)
       php: (projectId: string, domainCreateInput: DomainCreateInput)

📦 Class: IntegrationsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: createIntegration()
       node: (projectId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationCreateInput: IntegrationCreateCreateInput)

     Method: updateIntegration()
       node: (projectId: string, integrationId: string, type: string, params: IntegrationCreateData)
       php: (projectId: string, integrationId: string, integrationUpdateInput: IntegrationPatch)

📦 Class: MetricsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Missing Methods:
     php:
       - fetchMetrics()

📦 Class: ProjectsTask
   Languages: node, php
────────────────────────────────────────────────────────────────────────────────

  ⚠️  Signature Differences:

     Method: updateSettings()
       node: (projectId: string, settings: ProjectSettings)
       php: (projectId: string)

════════════════════════════════════════════════════════════════════════════════
  Summary
────────────────────────────────────────────────────────────────────────────────
  Total classes analyzed: 26
  ⚠️  Found 7 issue(s)
════════════════════════════════════════════════════════════════════════════════


@Theosakamg Theosakamg requested a review from flovntp June 5, 2026 08:58
@Theosakamg Theosakamg self-assigned this Jun 5, 2026
@Theosakamg Theosakamg added the bug Something isn't working label Jun 5, 2026
@Theosakamg Theosakamg merged commit 6237b11 into main Jun 5, 2026
10 checks passed
@Theosakamg Theosakamg deleted the fix/oauth2-refresh-token branch June 5, 2026 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants