The HubSpot script is loaded app-wide with loadImmediately: false, but
useHubSpotChat was never called, so widget.load() never ran. The old
HubSpotWidget shared the hs-script-loader id and was deduped by next/script.
Switching plan mid-trial always forced the monthly price and there was no
way to move to annual. Preserve trial_end on the Stripe update so changing
the interval does not reset the billing cycle and end the trial early.
Cloud runs two frontend replicas and node-schedule fires on each, so both
would read the same unset marker and every trial user would get the email
twice. A Postgres advisory lock lets one replica run and the other skip;
it lives in the connection rather than the schema, so no migration.
Drops the 3-day reminder, leaving the single notice before billing starts.
Stripe merges metadata on update, so spreading the snapshot read during
the list call re-asserted stale values over anything written since. Send
only the key being claimed.
The claim is not exclusive and the comment no longer says it is: two
replicas can still read the same unset marker. Marking before the send
only keeps a crash mid-send from resending on the next run.
Trials were always Hobby, so a trial user who wanted Startup had to go
through Checkout, which created a second subscription on the same
customer. Seven days later Stripe cancelled the trial sub and the webhook,
matching only on customer id, zeroed serversQuantity and deactivated the
servers of someone who was already paying.
The trial now starts on the plan the user chooses and switching plans
mutates that same subscription, so a customer never holds two.
- startFreeTrial takes a tier; TRIAL_SERVER_LIMIT becomes TRIAL_SERVER_LIMITS
(Hobby 1 server, Startup 3)
- onboarding offers both plans with a trial and a subscribe button
- billing exposes Switch to Startup/Hobby during the trial and hides the
checkout CTAs and server steppers that would create a second subscription
- upgradeSubscription caps the server count to the plan's included servers
while trialing and skips proration, which emitted an orphan invoice
- webhook treats trialing as active instead of disabling servers, and
guards invoices that carry no subscription
- getBillingStatus reports hasPaymentMethod, so the banners stop asking for
a card once one is on file
- new cloud-only daily job emails trial users without a payment method at 3
and 1 days left, deduped through subscription metadata claimed before the
send so concurrent replicas cannot double-send
Review flagged that multi-import collisions were untested. The order the code
implements is the documented one — Infisical: "If two imports carry a secret
with the same name, the value from the bottom-most import wins" — and the
response lists imports in that order, so sequential assignment matches it.
Added a case with two imports defining the same key. Reversing the merge order
in the client fails it, so the test pins the behaviour rather than restating
the implementation.
vault.test.ts: 58 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes#5413. A secret brought into a folder with "Import Secrets" was reported
as not found. Two reasons, and the first is easy to miss:
The list endpoint takes the flag in **snake_case**. `includeImports` is
silently ignored — the request still returns 200, with `imports` present but
empty — so the imported secret looked like it simply did not exist. Measured
against app.infisical.com with a registered import (confirmed via
GET /api/v1/secret-imports):
expandSecretReferences=true imports[] empty
expandSecretReferences=true&includeImports=true imports[] empty
expandSecretReferences=true&include_imports=true imports[] has the secret
Second, imported secrets never appear in `secrets` — they come back in a
separate `imports` array, one entry per source path, which the client did not
read at all.
Imported entries are merged before the folder's own, so a name defined in both
resolves to the local value, matching how Infisical resolves it.
Also measured, for whoever looks next: `/api/v4/secrets` returns imports with
no flag at all, and single-secret reads (`/raw/{name}`) never see an imported
key — 404 on v3, no such route on v4. So a folder listing is the only way to
reach them.
vault.test.ts: 57 passed. Reverting the fix fails the new merge test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a Transfer action on every service page that moves an application,
compose or database to another server without S3: volumes, bind mounts
and deployment logs are streamed through the panel (ssh2/spawn pipe),
file mounts and Traefik config are recreated on the target, the source
is cleaned up and the service is deployed on the target. Failures before
cleanup roll back to the source server.
An Infisical provider is pinned to one non-recursive `secretPath`, so reading
two folders means two providers, two machine identities and two sets of
credentials to rotate. This lets a reference name the folder instead:
${{vault.my-provider.external/sentry:SENTRY_DSN}}
`<path>:<KEY>` mirrors the HashiCorp client in this directory, which already
documents that exact format. A relative path resolves against the provider's
`secretPath`, a leading slash is absolute, and a ref without a colon keeps its
current meaning — the whole ref is the secret name at the provider's own path.
A dot cannot be the separator here: Infisical accepts dots inside secret names
(`A.B.C` is a valid key), so `provider.a.b.C` cannot be split unambiguously
and would silently break anyone using such a name.
Refs are grouped by resolved path so each path is listed once, and the login
happens once per batch rather than once per path.
Tests cover the bare ref, relative and absolute paths, a provider at `/`,
grouping with a single login, the error naming the path, and a malformed ref.
Previously, upsertRecord matched only by type and name, silently
overwriting any existing record of the same type/name with different
content. This caused data loss when users had multiple records of the
same type (e.g. multiple TXT records for SPF + site verification).
Now upsertRecord also checks the record content/target before deciding
to update. If the content differs, a new record is created alongside
the existing one instead of replacing it.
Affected providers: Cloudflare, Porkbun, Infomaniak, OVH.
Tests updated accordingly.
better-auth's account-linking guard requires either the IdP asserting
email_verified or the sso_provider row being domainVerified before it
will link an SSO login to an existing local user with the same email.
Entra ID never asserts email_verified (neither via its OIDC userinfo
endpoint nor via SAML attributes), and Dokploy never persisted
domainVerified at all, so linking was permanently impossible for any
Entra-backed provider (OIDC or SAML) regardless of the local user's
own emailVerified state.
Add the domainVerified column, defaulting to true — Dokploy already
scopes providers to an admin-configured domain and gates registration
behind enterprise/admin permissions, so that domain match (still
enforced by better-auth's validateEmailDomain check) is trust enough.
Fixes#5099
Fixes#5279. The Domains table (and grid) view mounts one AddDomain
instance per row for the edit action. Its 4 queries (domain.one,
application.one/compose.one, domain.canGenerateTraefikMeDomains,
compose.loadServices) ran unconditionally on mount instead of only
when the edit dialog is open, so any table interaction that causes a
re-render (typing in the host filter, sorting, toggling columns) fired
all 4 queries for every domain row again.
A secret in Infisical can reference another one (`${env.folder.KEY}`), which
is the only way to keep a value in one place and read it from several folders.
The Infisical client fetches `/api/v3/secrets/raw` without
`expandSecretReferences`, and that endpoint leaves references untouched by
default. The referencing secret therefore arrives as the literal `${...}`
string, is written to the generated `.env`, and the deployment still reports
success — the service just receives a placeholder instead of its value.
This is easy to miss because the single-secret endpoint (`/raw/{name}`) does
expand by default, so the Infisical UI and API show the resolved value while
only Dokploy sees the literal.
Adds the parameter and the first tests for this client: the list request now
asserts the flag, a missing secret and an auth failure. Without the fix the
new test fails with `expected null to be 'true'`.
upsertRecord pulled every record of a zone just to find the one it was about to
write. The API can filter, so ask it to.
The match is still verified locally, because filter[source] matches substrings
rather than exact values. Verified against a live account:
filter[source]=autoconfig -> [autoconfig]
filter[source]=auto -> [autoconfig, autodiscover]
filter[source]=disco -> [autodiscover]
Letting the filter decide would therefore have upserted "auto" onto the existing
"autoconfig" record. The filter narrows what is transferred; the exact
comparison still decides. Covered by a test that fails if the first candidate is
trusted.
The apex was the other thing worth checking, since Infomaniak writes it as ".":
filter[source]=. does return the apex records, and an upsert on an existing apex
TXT returns its id rather than creating a second one.
Suggested by @narcisonunez on #5257.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#5257 merged, so canary now carries the Infomaniak provider and its
0191_cool_christian_walker. This branch's 0191_elite_ultragirl collided on the
number, and the conflicts were the usual "infomaniak and ovh added at the same
spot" across the enum, the config union, the client registry, SENSITIVE_FIELDS,
both providerLabels maps, the icons and the provider form — all resolved by
keeping both.
Regenerated as 0192_light_lake with a current timestamp rather than renaming the
file, so drizzle still applies it on a database that has already run canary's
0191.