* feat(plugin): add Fastly custom TLS certificates integration
Introduces a complete set of Fastly plugins for automating custom TLS certificates deployment:
- `FastlyAccess`: Handles Fastly API authentication and provides helpers for dynamic data fetching (Services, Domains, Configs).
- `FastlyUploadCert`: Uploads a new private key and certificate (2-step POST process) or updates an existing one (PATCH).
- `FastlyDeployCert`: Activates a custom TLS certificate by linking it to a TLS configuration and domain via TLS Activations API.
- `FastlyRefreshCert`: Bulk updates existing custom TLS certificates.
- `FastlyPurgeCache`: Purges all cache for a specified Fastly Service.
Includes comprehensive unit tests covering API request formats and error handling for all tasks.
* fix(fastly): drop invalid 'multiple' option from refresh-cert input define
createRemoteSelectInputDefine has no 'multiple' option; multi-select is
already its default (mode: "tags", single: false). The excess property
broke the production build (TS2353).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015dRf6EbAmwb37cpnvgBDrh
* fix(fastly): correct TLS list unwrapping, paginate, harden purge
- access: ctx.http already unwraps to the response body, so the JSON:API
item array is at body.data, not body.data.data — getTlsConfigurations /
getTlsDomains / getCertificates always returned [] and the selectors
were empty. Fetch via listAllJsonApi, which also pages past Fastly's
20-item default so accounts with more than one page are fully listed.
- purge-cache: send no body to /purge_all (auth is header-only) instead
of an empty JSON:API object.
- upload-cert / refresh-cert: note in the helper text that Fastly requires
the renewed cert to reuse the original private key for PATCH updates.
- test: cover the list-helper unwrapping, pagination aggregation, and the
bare-array /service response.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015dRf6EbAmwb37cpnvgBDrh
* fix(fastly): use output-selector for deploy cert ID input
The FastlyDeployCert `certificateId` input was a free-text box whose
helper pointed users at a non-existent "advanced settings" workflow to
grab the upstream output variable. certd only surfaces a prior step's
@TaskOutput through an input whose component is `output-selector`, so the
FastlyUploadCert `fastlyCertId` output was never selectable.
Bind the input to `output-selector` with `from: ["FastlyUploadCert"]`,
matching the pattern used by plugin-aliyun deploy-to-cdn.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J
* fix(fastly): accept cert or uploaded id in deploy, share upload helpers
Rework FastlyDeployCert to mirror the AWS CloudFront pattern instead of
demanding a hand-typed / separately-uploaded certificate id:
- The `certificateId` text box (and the earlier FastlyUploadCert-only
output-selector) is replaced by a `cert: CertInfo | string` input whose
output-selector accepts both a cert-apply step's cert and a
FastlyUploadCert step's `fastlyCertId`.
- execute() binds the id directly when it got a string; when it got a raw
CertInfo it uploads to Fastly first (new optional `name` input), then
creates the TLS activation. So a plain "apply cert -> activate" pipeline
needs no separate upload step.
Extract the Fastly certificate create (2-step key + cert) and update
(PATCH) flows into FastlyAccess.createCertificate / updateCertificate and
reuse them from both FastlyUploadCert and FastlyDeployCert.
Tests updated to drive the plugins through a real FastlyAccess with only
doRequestApi stubbed; added a deploy case for the raw-CertInfo path.
Note: build/tests not run locally (no Node toolchain on this machine) —
needs CI / local verification.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J
* fix(fastly): reuse existing key/cert when Fastly reports "already exists"
Fastly deduplicates TLS private keys (and byte-identical certificates) by
content, so re-running an upload fails with 400 "... already exists:
'<id>'". Recover that id and carry on instead of aborting:
- ensurePrivateKey(): on "already exists", parse the id from the error
(or match an existing key by name), else throw actionable guidance.
- createCertificate(): same treatment for the /tls/certificates POST;
when no id is recoverable, tell the user to supply the certificate id
so the PATCH (update) path is used.
- Shared FastlyAccess.extractExistingId() helper.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J
* fix(fastly): per-domain TLS activation, idempotent on renewal
The TLS 域名 / TLS 配置 selectors use createRemoteSelectInputDefine, which
defaults to multi-select ("tags"), so domainId arrived as an array and was
written straight into tls_domain.data.id -> Fastly 500. A tls_activation
also binds exactly one domain, so wildcard + apex needs one each.
- domainId -> domainIds (string[]); loop, one activation per domain.
configurationId gets single:true; both inputs are still normalised in
execute() (array-or-scalar, plus legacy `domainId` key).
- New FastlyAccess.deployActivation(): looks up an existing activation for
the domain+configuration and PATCHes it onto the new certificate;
otherwise POSTs a fresh one. Renewals no longer fail on a duplicate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J
* feat(fastly): list service domains for TLS activation, not cert SANs
The TLS 域名 selector was populated from GET /tls/domains, which only
echoes certificate SAN entries — so a wildcard cert offered "*.g0l.net",
which cannot be activated unless a service actually serves that wildcard.
onGetTlsDomainList now lists the domains configured on the active version
of each Fastly service (new FastlyAccess.getServiceDomains()), which are
the names you can actually activate (e.g. www.g0l.net, covered by the
wildcard). Cert-derived TLS domains are still appended, tagged "(来自证书)".
The field stays free-entry so an unlisted host can be typed in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Parse PEM chain and import certificate chain
Split the PEM in certInfo.crt into a leaf certificate and intermediate chain (using a lookbehind regex), trim the blocks, and pass the chain to ImportCertificateCommand only when present. Replace console.log with this.logger.info and log the returned CertificateArn. This ensures the leaf cert is uploaded separately from its chain and avoids sending an empty CertificateChain.
* Add AWS retry & CloudFront deployment wait
Introduce robust retry and polling helpers to handle AWS throttling and CloudFront propagation. Added AwsClient.withRetry (exponential backoff, handles common throttling errors, default 5 attempts/base 2s) and waitForDistributionDeployed (polls until distribution Status is "Deployed", default 10min timeout/15s interval). Update deploy-to-cloudfront plugin to use withRetry for Get/UpdateDistribution and importCertificate, pass AwsClient into uploadToACM, and wait for each distribution to finish deploying before continuing to avoid PreconditionFailed errors. Improves reliability when facing rate limits and global CloudFront propagation delays; adds informative logging for retry and deployment status.
* fix(aws): resolve rate limit and runtime import bugs
* fix(aws): broaden retry detection and harden CloudFront poll
- withRetry: retry on Route53 PriorRequestNotComplete, SlowDown, SDK v3
$retryable.throttling hints and HTTP 429/503; extract detection into
isRetryableAwsError
- withRetry: add equal jitter to backoff so concurrent pipelines don't
retry in lockstep
- waitForDistributionDeployed: a throttle that outlives withRetry's budget
keeps polling until the deadline instead of aborting the deploy
- test: cover withRetry retry/exhaustion/non-throttle paths
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015dRf6EbAmwb37cpnvgBDrh
* fix(aws): fix build break and make withRetry the sole retry authority
- withRetry<T = any>: default the type param so call sites passing an
untyped SDK client (@aws-sdk/* is a lazyDependency with no build-time
types) don't collapse the result to `unknown`. Also annotate the two
result consumers as `any`. Fixes the TS2339 errors that broke the
Docker build (aws-client.ts, plugin-deploy-to-cloudfront.ts).
- Set maxAttempts: 1 on every AWS SDK client (ACM/STS/Route53/CloudFront)
so the SDK's own retry strategy no longer stacks on top of withRetry.
- isRetryableAwsError: broaden to cover what the SDK's `standard` strategy
used to handle now that it's disabled - $retryable hint, HTTP 429 + any
5xx, connection-level errors (ECONNRESET/ETIMEDOUT/EAI_AGAIN/...).
- waitForDistributionDeployed: a transient error that outlives withRetry's
budget now retries on the next poll instead of aborting the whole deploy.
- test: cover 5xx, $retryable, and connection-error retry paths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrMVmQyvQjes6t4DGjuBaD
* feat(aws): make CloudFront deploy-wait opt-in (default off)
Polling GetDistribution until Status=Deployed adds 3-8min of pipeline
wall-time per run for no functional gain - CloudFront finishes the
rollout on its own once UpdateDistribution returns 200. Gate it behind
a new "等待部署完成" switch, default false. Retry/backoff on the actual
mutating calls (GetDistributionConfig / UpdateDistribution / ACM import
/ Route53) is unaffected - it wraps those sends directly, not the poll.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrMVmQyvQjes6t4DGjuBaD
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>