Skip to main content

API Changelog

Stay up-to-date with the latest changes, improvements, and new features in the Trading Card API itself.

API vs Documentation Changes

This page shows changes to the Trading Card API service. For changes to this documentation site, see the Documentation Changelog.


Latest Releases​

All notable changes to the Trading Card API are documented here. The API follows Semantic Versioning and maintains a Keep a Changelog format.


🚀 Recent Updates​

📦 [0.11.0] - October 11, 2026​

Latest Release

This is the current production version of the Trading Card API.

Added​

  • [Issue #2223] Add the unauthenticated password reset flow: POST /v1/user/password/forgot mints a single-use expiring opaque token and emails a portal reset link, and POST /v1/user/password/reset redeems it, sets the new password, and revokes all of the user's Passport sessions.
  • forgot always returns 202 regardless of whether the address matches an account, so it cannot be used to enumerate users; it is throttled per IP via the new RATE_LIMIT_PASSWORD_FORGOT limiter, and the reset email needs LOOPS_RESET_PASSWORD_TEMPLATE_ID set to a Loops transactional template.
  • [Issue #2224] Accept a client-supplied card UUID on create and add a scope-gated bulk card upsert-by-id endpoint with per-row results.
  • Ordinary API tokens may pin a new card's id on POST /v1/cards (a taken id returns 409, never an overwrite); only the internal-scoped bulk endpoint can upsert over an existing id.
  • [Issue #2222] Add a user:reset-password {email} artisan command so an operator can reset a locked-out customer's password and revoke their OAuth tokens without a production tinker session.
  • Operator-only: the command has no HTTP route and must be run on the API host; a generated password is printed once and must be transmitted out-of-band.
  • Opaque tc_ data keys are left authenticating by default, matching the self-service change-password behaviour, so a routine reset does not take a customer's live API integration down. When a reset leaves keys alive the command reports how many and how to revoke them; pass --revoke-data-keys to revoke them as part of the reset when an account may be compromised.
  • [Issue #1916] Add a back-merge-stable-to-develop GitHub Actions workflow that back-merges master into develop on every push to master, pushing directly when the merge is clean and opening a conflict PR otherwise, so develop never silently falls behind the stable branch.
  • The sync is one-way by design: develop -> master promotion stays a deliberate release step and is not automated.
  • [Issue #2431] Add a read-only catalog:doctor artisan command that generates catalog publication, population, taxonomy, vocabulary, and entity name-quality figures.
  • Exits non-zero when an always-wrong anomaly is present, so it is usable as a scripted assertion.
  • [Issue #2369] Report published top-level sets whose name extends an existing draft or flagged base set in catalog:doctor, with a per-year breakdown.
  • [Issue #2669] Add an --expect-unfiltered mode to verification:compare-gate that records a privileged consumer token's effective scope and expects a zero delta against the reference token.

Changed​

  • [Issue #2383] Reverse the 2026-08-27 decision to leave players, teams and player-teams ungated, recording in docs/STATUS-FILTERING-IMPROVEMENT.md that no endpoint may return anything derived from a draft set.
  • Scope caveat: the cheap half (brands, manufacturers, years and genres) is split out to #2430 and is not blocked on this work.
  • [Issue #2485] Record ADR-006 selecting a GitHub merge queue as the fix for stale-but-green PRs reddening master, and add the merge_group trigger the required test contexts need.
  • Enabling the queue itself is an operator settings change; the ADR carries the checklist, and the merge_group trigger must be merged to master before the queue is turned on.
  • [Issue #2674] Baseline /v1/object-attributes in verification:compare-gate and report gate-off and no-data rows distinctly from passes.
  • [Issue #1916] Sync the 0.10.1–0.10.34 stable releases back into develop across three manual back-merges, resolving the rate-limiter, internal-route, CHANGELOG.md and build/scripts/test-build-scripts conflicts as unions.
  • Development-line housekeeping only: every change these syncs carried already shipped on master in its own 0.10.x release.

Fixed​

  • [Issue #2253] Stamp the released version into the served OpenAPI spec at image build time and correct public /api/v1 references to /v1.
  • The committed public/docs/openapi.json deliberately keeps its 0.0.0-dev sentinel; only the built image carries the real version.
  • [Issue #2312] Stop the pre-PR gate from markdownlinting dot-directory files that CI's glob never reaches.
  • [Issue #2421] Publish the beta Docker image and GitHub release before committing the changelog back to develop, so a transient build failure no longer strands the version with a changelog section but no tag, image, or release.
  • The commit-back step is now non-fatal: if the push is rejected the job emits an actionable ::warning instead of failing, and re-running the job is safe.
  • [Issue #2542] Number develop beta releases from the highest beta tag that already exists for the target version instead of from the nearest reachable tag, so a master sync merge can no longer rewind the counter into a tag collision or, worse, a green build published behind the beta already released.
  • Beta numbers may jump further than before on the first develop build after a sync merge; they remain strictly increasing within a series, and mid-series numbering is unchanged.
  • [Issue #2544] Stop forwarding failed bearer-token validation to the external error reporter, so a malformed or expired token no longer emits a Sentry event and an error-level log line on every request.
  • Passport's TokenGuard reports League's OAuthServerException, not the Passport wrapper listed in $dontReport; the handler now suppresses client-caused (4xx) OAuth failures while still reporting genuine 5xx server_error cases.
  • [Issue #2227] Bake the Loops reset-password template id into the image so forgot-password emails are actually sent.

Security​

  • [Issue #2484] Write card image objects privately and serve them through short-TTL signed CDN URLs so the storage grant expires with the API gate.
  • Image URLs in API responses now expire; consumers that persist a download_url, links.* or variants[].url beyond the TTL must re-fetch the record. Set IMAGE_SIGNED_URLS_ENABLED=false to restore unsigned URLs, and run php artisan images:backfill-visibility to re-ACL objects already written public-read.

📦 [0.10.113] - October 10, 2026​

Added​

  • [Issue #2730] Add an internal-scope GET /internal/analytics/customer-usage endpoint reporting per-customer request counts over a date range.

Fixed​

  • [Issue #2657] Reject any api_sets.status value outside draft/published with a DB-level constraint and a model-level guard.
  • [Issue #2670] Exclude soft-deleted player-teams from /v1/player-teams by adding SoftDeletes to PlayerTeam.
  • DELETE /v1/player-teams/{id} now soft-deletes; re-posting the same pair restores the original row.
  • [Issue #2736] Trigger Build and Test on merge_group events on master so merge-queue PRs get their required Unit Tests and Integration Tests checks.

📦 [0.10.111] - October 10, 2026​

Added​

  • [Issue #2650] Add an actionlint CI job to lint.yaml so a workflow file that fails to parse is caught on the PR instead of silently never running in production.
  • Also fixes the inert workflow_dispatch.paths key in sync-labels.yaml, one of only two pre-existing actionlint findings in the repo.

📦 [0.10.110] - October 9, 2026​

Fixed​

  • [Issue #2616] Skip tagging, building, and publishing a release when a push to master collates no changelog.d/ fragments, so release-tag numbers stay in lockstep with CHANGELOG.md sections.
  • A push whose only content is a Dependabot or skip-changelog-labeled PR (neither of which carries a fragment) no longer cuts its own release tag — it waits for a later push that does.

📦 [0.10.109] - October 8, 2026​

Fixed​

  • [Issue #2592] Fix the pre-PR gate's MOUNTS docker-run argument list to survive zsh's non-word-splitting unquoted expansion by declaring it as a shell array instead of a scalar string.
  • Covers the MOUNTS argument list only; the markdownlint mounts and the targeted-phpunit --filter still rely on scalar word-splitting under zsh and are tracked in #2714.

📦 [0.10.108] - October 8, 2026​

Added​

  • [Issue #2625] Add a MySQL-backed PHPUnit CI lane (#[Group('mysql')]) so driver-specific schema bugs can no longer pass green on SQLite alone.
  • Opt-in: existing tests are unaffected unless tagged #[Group('mysql')]; a local make test-mysql target is also available.

📦 [0.10.107] - October 8, 2026​

Added​

  • [Issue #2562] Add .claude/version-bump.md declaring that CI — not the PR — owns the version bump.

📦 [0.10.106] - October 7, 2026​

Fixed​

  • [Issue #2561] Fix the public API reference omitting unauthenticated /v1/stats endpoints by classifying docs per-operation on explicit security: [] instead of per-tag.
  • [Issue #2611] Report catalog:rebuild-visibility's no-throwable failures (bad --chunk, bad --table, lock already held) to the error-reporting seam so they are no longer invisible in Sentry.

Security​

  • [Issue #2550] Pass workflow VERSION values through env: instead of interpolating them directly into run: shell commands, closing a script-injection surface in the release workflows.
  • Defense-in-depth only — VERSION is derived from git tag names, which already require push access.

📦 [0.10.104] - October 6, 2026​

Fixed​

  • [Issue #2679] Recompute published-entity visibility when a base card's cascade-deleted parallel children belonged to a different, currently-published set.
  • Functionally depends on #2656's relaxation of RebuildVisibilityForSet::affectedIds()'s deleted_at filters, which has already landed (PR #2688).

📦 [0.10.103] - October 6, 2026​

Fixed​

  • [Issue #2539] Have the pre-PR gate generate ephemeral Passport keys before its targeted PHPUnit stage, instead of letting the storage/ mount hide the image's baked keys.
  • Self-healing per workspace; the generated storage/oauth-*.key files stay gitignored.

📦 [0.10.102] - October 3, 2026​

Fixed​

  • [Issue #2575] Stop a pending signup from resending or verifying against an invite the operator has revoked.
  • Revocation is non-enumerating, same as an unknown code — reclaim() returns invite_invalid, not a distinct "revoked" error.

Security​

  • [Issue #2557] Pin nanoid, @faker-js/faker, and axios to patched versions via npm overrides, closing the 3 open security advisories with no covering Dependabot PR.

📦 [0.10.101] - October 2, 2026​

Fixed​

  • [Issue #2675] Fail verification:compare-gate per date when a customer-scoped stats series count exceeds the reference count, instead of only checking each series' net sum.
  • Corrects two existing tests that previously asserted a per-date customer-above-reference reading was not a leak.

📦 [0.10.100] - October 2, 2026​

Fixed​

  • [Issue #2565] Fix invite:issue --never-expires crashing after issuing the invite, caused by . binding tighter than ?: in the Expires: report line.

📦 [0.10.99] - October 2, 2026​

Fixed​

  • [Issue #2655] Maintain published_visible for sets published through bulk validation, without dispatching a catalog-wide rebuild job.

📦 [0.10.98] - October 2, 2026​

Fixed​

  • [Issue #2540] Refuse a checkout for a customer who already holds an entitling subscription, preventing the duplicate Stripe subscription and double charge that caused #2536.

📦 [0.10.97] - October 2, 2026​

Fixed​

  • [Issue #2645] Install the pcntl PHP extension in the app image so queue:work's per-job timeout is actually enforced and SIGTERM handling is enabled, instead of both silently no-op'ing.
  • Enables SIGTERM handling, not guaranteed graceful shutdown — laravel-worker.supervisord.conf sets no stopwaitsecs, so Supervisord's 10s default can still SIGKILL a job that isn't done cleaning up in time; safe to ship now regardless, since the job-timeout/worker-timeout/retry_after ordering this enforcement depends on was already raised for RebuildVisibilityForSet by #2644 (PR #2647).

📦 [0.10.96] - October 2, 2026​

Fixed​

  • [Issue #2656] Trigger a published-visibility recompute when a published set is soft-deleted or force-deleted, instead of only on publish/unpublish.
  • The bounded monotonic-update shortcut used for #2653-style cheap updates does not apply here — this direction requires the full reverse computation (7–13 min), same as the existing publish path.

📦 [0.10.95] - October 2, 2026​

Fixed​

  • [Issue #2652] Run RebuildVisibilityForSet on a dedicated maintenance queue and single worker so long visibility rebuilds no longer block customer-facing jobs.
  • Takes effect on the next image deploy; rebuilds already queued on default before the deploy still drain there.

📦 [0.10.94] - October 1, 2026​

Fixed​

  • [Issue #2642] Add the low queue to the production worker's queue list so backlogged LogApiUsage jobs are consumed instead of growing unboundedly.
  • default keeps strict priority — low is only polled when default is empty, so this does not delay current-traffic jobs.

📦 [0.10.93] - October 1, 2026​

Fixed​

  • [Issue #2654] Maintain published_visible synchronously when a card or on-cardable row is attached to an already-published set.
  • The maintenance is a bounded, indexed UPDATE on the affected rows only — it never dispatches the catalog-wide RebuildVisibilityForSet job.

📦 [0.10.92] - September 30, 2026​

Added​

  • [Issue #2665] Add verification:compare-gate and a runbook for re-verifying the six draft-gate consumers against the live gate with the customer-grade key.

📦 [0.10.91] - September 28, 2026​

Fixed​

  • [Issue #2629] Fix a newly set taxonomy value (genre, manufacturer, brand, year) being invisible on the next GET /v1/sets/{id} read by bypassing the usage-gated scope for a set's own direct relations.

📦 [0.10.90] - September 26, 2026​

Fixed​

  • [Issue #2641] Fix .github/workflows/renew-tls-cert.yaml, which failed at workflow startup on every run and so never once executed the production TLS renewal automation added in #2620: a shell comment inside a run: block contained a literal empty GitHub Actions expression, which GitHub expands before bash ever sees the script, and the expression parser rejected it — failing the whole file at parse time.
  • The 17 4 * * * schedule has never fired and the Slack failure alert lives inside the job that was never created; merging this is what makes the first scheduled renewal run possible.

📦 [0.10.89] - September 26, 2026​

Fixed​

  • [Issue #2644] Stop the queue re-reserving RebuildVisibilityForSet mid-run: the database connection's retry_after (90s) was far below the job's real runtime (13m17s observed), so DatabaseQueue treated the row as expired and the second worker ran a concurrent copy of the same job, leaving the default queue with no spare capacity.
  • retry_after is now 1860s, the worker runs with --timeout=1800, and the job declares $timeout = 1800 plus a set-keyed overlap guard that releases (never drops) a colliding dispatch. The trade: a worker killed without cleanup now leaves its row reserved for up to 31 minutes.

📦 [0.10.88] - September 26, 2026​

Fixed​

  • [Issue #2643] Add a (queue, id) index to the jobs table so Laravel's queue-pop query stops full-scanning the primary key: the pop for queue=default measured 6.2s in production against a 1.2ms baseline, draining roughly one job every three seconds.
  • Deploy note: stop the laravel-worker program before migrating and restart it after — the ALTER needs a brief exclusive metadata lock and must wait on the in-flight ~6s pop transactions, so expect a stall of several seconds.

📦 [0.10.87] - September 26, 2026​

Added​

  • [Issue #2608] Add verification:provision-key so a customer-grade opaque API key can be provisioned without an interactive portal session, and pin its scope set against future privilege drift.
  • The provisioned user deliberately has no billing account, so the credential passes the subscription gate but cannot be used to verify the 402 path.
  • [Issue #2609] Define the v1 filter-set convention in ADR-007, add exact-match filter[name] to /v1/cards, and conform teams and player-teams to it.
  • Routes previously unvalidated (/v1/genres, /v1/teams, /v1/player-teams) now reject unknown query parameters with a 400 instead of ignoring them.
  • /v1/cards is a documented exception to the uniform like: rule: filter[name]=like:… returns a 400, because the leading-wildcard LIKE the operator compiles to cannot use the existing index on a ~1.77M-row table. Substring search is tracked at #2590.

Fixed​

  • [Issue #2606] Return a null include instead of a 500 on /v1/player-teams when a pivot's player or team is hidden by the status gate, and make the per-set visibility refresh update all three tables atomically.

📦 [0.10.85] - September 26, 2026​

Fixed​

  • [Issue #2541] Fix a 500 error when ?include[]=sets (array-form query param) is sent to /v1/brands, matching the string-form ?include=sets behavior.

📦 [0.10.84] - September 26, 2026​

Fixed​

  • [Issue #2559] Report the per-billing-account plan window in GET /v1/user/usage alongside the tc_ key window.

📦 [0.10.83] - September 25, 2026​

Added​

  • [Issue #2601] Renew the production TLS certificate on a daily schedule and fail the run when the live certificate is inside a 21-day expiry horizon.
  • Renewal runs out-of-band from .github/workflows/renew-tls-cert.yaml; the entrypoint hook in .docker/scripts/update.entrypoint.sh stays disabled deliberately, because it stalls the renewing deploy for up to 8 minutes.

Fixed​

  • [Issue #2556] Return a null player or team relation instead of a 500 when a player-team's player or team is hidden, soft-deleted, or missing.

📦 [0.10.82] - September 21, 2026​

Fixed​

  • [Issue #2623] Exclude MySQL generated columns from Card::replicate() so copying a card into a parallel set no longer fails with a 3105 INSERT error.

📦 [0.10.81] - September 19, 2026​

Added​

  • [Issue #2555] Add filter[team_id] to GET /v1/cards and a GET /v1/teams/{team}/cards convenience route, returning every card a team appears on both directly and via one of its player-teams.

📦 [0.10.80] - September 18, 2026​

Fixed​

  • [Issue #2613] Exclude null pivot ids from the published-visibility id set so catalog:rebuild-visibility completes the players and teams stages.

📦 [0.10.79] - September 18, 2026​

Fixed​

  • [Issue #2591] Deduplicate the published-visibility id set per branch so the nightly catalog:rebuild-visibility rebuild of api_player_teams no longer violates the staging table's primary key.
  • The command now reports the underlying exception to Sentry directly rather than through the report() helper, which suppresses unique-constraint failures.

📦 [0.10.78] - September 14, 2026​

Changed​

  • [Issue #2603] Enable the on-cardable status gate by default in production so /v1/players, /v1/teams and /v1/player-teams filter on the materialised published_visible signal rather than returning every entity.
  • #2603 deliberately kept the flag out of the repository variables so it stays reviewable in a diff, so the deploy workflow never forwards it; setting it to false on the host disables the gate until the next deploy, and the durable change is editing the compose default and cutting a release. The nightly catalog:rebuild-visibility backstop stays broken until #2591, so the endpoints reflect that signal's freshness in both directions: a missed refresh can leave a newly published entity hidden, or leave an entity from a since-unpublished set still exposed.

📦 [0.10.77] - September 12, 2026​

Fixed​

  • [Issue #2597] Rebase and retry the release changelog commit-back when master moves mid-run, so a race can no longer drop a release's ## [x.y.z] section, and restore the 20 released versions that were missing one.
  • Sixteen of those (0.10.25 through 0.10.74) lost their section to a rejected push and were collated into the next release's section; the other four (0.8.46 through 0.8.49) predate the commit-back step entirely (#1898) and are restored from their GitHub Release bodies. The retry is opt-in (--rebase-on-reject), used only by the release path, and refuses to rebase over a racing change that touched generated documentation.

📦 [0.10.76] - September 12, 2026​

Fixed​

  • [Issue #2595] Restore the master test suite by implementing InviteGate::reclaim() on the race-staging invite-gate double that #2288 left behind when it widened the contract.
  • Test-only: the double delegates the method to the real service, so no runtime behavior changes.

📦 [0.10.75] - September 10, 2026​

Added​

  • [Issue #2460] Add --preview and --sleep options to cleanup:backfill-set-identity so the draft-scope backfill can be reviewed in full before it is applied and paced when it is.
  • --preview is honoured only alongside --dry-run; outside a dry run it warns and is ignored.

📦 [0.10.74] - September 10, 2026​

Fixed​

  • [Issue #2511] Correct the v2 Bruno checklist docs to list all five fields format=compact returns, not three.

📦 [0.10.73] - September 10, 2026​

Fixed​

  • [Issue #2448] Remove the github.token composer-auth fallback so Dependabot runs fall back to anonymous package resolution instead of a credential github.com rejects.
  • The root-cause fix is still an operator action outside this repo: rotate or delete the org-level Dependabot COMPOSER_TOKEN.

📦 [0.10.72] - September 10, 2026​

Fixed​

  • [Issue #2288] Re-assert the caller's invite claim on POST /v1/signup/resend so a released hold cannot be redeemed later and exceed the invite's max_uses.
  • A resend for an invite that has since been fully claimed by someone else is now refused with 422 invite_exhausted; it previously rotated the token and sent a new link.

📦 [0.10.71] - September 10, 2026​

Fixed​

  • [Issue #2287] Serialize invite claim acquisition under a row lock so racing signups can no longer exceed an invite's max_uses.
  • A signup that loses the race is now rejected with invite_exhausted and its account is rolled back; InviteGate::claim() returns bool instead of void.
  • [Issue #2534] Accept the like: operator prefix on the /v1/players first_name, last_name and full_name filters so a partial surname narrows results instead of returning nothing.
  • Exact match remains the default and no query parameter is added, so existing callers are unaffected.

📦 [0.10.70] - September 10, 2026​

Security​

  • [Issue #2499] Resolve the object-attribute write target before mutating it, and pin that the #2434 status gate closes PATCH/DELETE /v1/object-attributes/{id} for a read:published token.
  • Behavioural only while STATUS_GATE_OBJECT_ATTRIBUTES_ENABLED=true, which still defaults to false pending the #2434 production measurement.

📦 [0.10.69] - September 10, 2026​

Changed​

  • [Issue #2298] Make /v1/stats/counts, /v1/stats/snapshots and /v1/stats/growth public so aggregate marketing data no longer depends on a credential.
  • Anonymous callers receive the existing published-only view (total equals published, draft is 0); rate limiting still applies and API usage logging is skipped.

Fixed​

  • [Issue #2535] Emit relationship links.self and links.related with the /v1/ version segment instead of dropping it.
  • Response bytes change for any resource whose transformer supplies no custom links block; its top-level self becomes versioned too.

📦 [0.10.68] - September 10, 2026​

Fixed​

  • [Issue #2278] Apply a subscriber's plan rate limit to Personal Access Token and OAuth user tokens, not only to tc_ API keys.
  • Tightens an existing allowance: a subscriber calling with a PAT was previously metered at the flat authenticated tier and will now be metered at their plan tier, bucketed per billing account.

📦 [0.10.67] - September 9, 2026​

Fixed​

  • [Issue #2437] Document every status-gated endpoint, the kill switch that controls it, and that switch's current default, and guard the record against drift.

📦 [0.10.66] - September 9, 2026​

Deprecated​

  • [Issue #2326] Deprecate bare filter spellings on /v1/players and /v1/sets in favor of one bracketed filter[...] convention across v1.
  • Bare spellings keep working for at least one release and now return Deprecation and Sunset response headers; they are removed in a follow-up.

📦 [0.10.65] - September 9, 2026​

Changed​

  • [Issue #2386] Restate the published catalog figures on a like-for-like basis and document GET /v1/stats/counts as the canonical source for the published-catalog size.
  • The previously quoted 273 counted top-level sets only, against a card total spanning every level; the like-for-like pair is 4,781 published sets and 325,898 published cards.

📦 [0.10.64] - September 9, 2026​

Added​

  • [Issue #2301] Record the submitted email address when POST /v1/signup returns the founding-pool waitlist response, so signups that arrive after the pool fills are no longer lost.
  • Addresses land in a new internal founding_waitlist_entries table only; they are not enrolled into any marketing list and are never emailed, and the waitlist response body is unchanged.

📦 [0.10.63] - September 9, 2026​

Added​

  • [Issue #2300] Add an invite:revoke artisan command that revokes a live signup invite by id or code, so killing a stale invite no longer needs a hand-written production database write.
  • Adds revoke() to the InviteGate contract; any class implementing that interface directly must add the method.

📦 [0.10.62] - September 9, 2026​

Changed​

  • [Issue #2537] Card the automated Sync master into develop conflict PR onto the CardTechie project board at 👀 In review, with a sync label, so it enters the normal work queue instead of living only in the PR list.
  • Best-effort: a PROJECT_TOKEN without project scope still opens the PR, logs a ::warning, and exits 0.

📦 [0.10.61] - September 7, 2026​

Security​

  • [Issue #2383] Gate /v1/players, /v1/teams and /v1/player-teams on a materialised published_visible column so entities appearing only on unpublished cards are hidden from customer credentials.
  • Ships default-off behind STATUS_GATE_ON_CARDABLE_ENABLED: the gate must not be enabled until the production EXPLAIN and unbounded COUNT evidence #2373 requires has been captured with php artisan status-gate:measure-on-cardable.

📦 [0.10.60] - September 7, 2026​

Fixed​

  • [Issue #2355] Gate the develop beta build on validate-changelog-ready before the Docker push, so a computed version that is already a released tag fails fast instead of overwriting a published image.

📦 [0.10.59] - September 6, 2026​

Fixed​

  • [Issue #2536] Stop a customer.subscription.deleted event for a non-mirrored subscription from revoking API access for the whole billing account.
  • Deleting the mirrored subscription now re-reads the customer's subscriptions from Stripe and re-points to a surviving active one, so the webhook handler makes an outbound Stripe call on that path; billing:resync-subscription repairs rows already corrupted by the old behavior.

📦 [0.10.58] - September 5, 2026​

Changed​

  • docs: normalize "Behaviour" to "Behavior" in CHANGELOG.md
  • docs: update CHANGELOG and docs for 0.10.57 [skip ci]

📦 [0.10.57] - September 5, 2026​

Added​

  • [Issue #2429] Add a reversible cleanup:dedupe-brands command and document the manufacturer/brand naming convention.
  • Ships tooling and documentation only; running the command against production data is a separate operator action, and api:update-names --sets must follow any execute or rollback.

📦 [0.10.56] - September 5, 2026​

Changed​

  • [Issue #2428] Backfill manufacturer and brand on published top-level sets via the new cleanup:backfill-set-identity command.
  • Rendered set names change for affected rows (e.g. 1996-97 Black Diamond becomes 1996-97 Upper Deck Black Diamond); the command is dry-run by default and writes a rollback file before applying.

📦 [0.10.55] - September 5, 2026​

Security​

  • [Issue #2434] Derive /v1/object-attributes visibility from the publication status of the set each attribute resolves to.
  • Off by default: set STATUS_GATE_OBJECT_ATTRIBUTES_ENABLED=true only after recording the production measurement from php artisan status-gate:measure-object-attributes on issue #2434.

📦 [0.10.54] - September 5, 2026​

Fixed​

  • [Issue #2435] Gate the v1 stats endpoints on the caller's status scope so draft volume is no longer reported and /v1/stats/{type} no longer bypasses the status filter.
  • Cached stats responses are now keyed per status posture, so any entry cached before this release is superseded rather than reused.

📦 [0.10.53] - September 5, 2026​

Security​

  • [Issue #2436] Scope bulk card-image batch reads to the caller that created the batch, returning 404 for batches owned by anyone else.
  • Batches created before this change have no recorded owner and become unreadable; api_card_images held 0 production rows as of 2026-08-26 (#2385), so there is no real data to preserve.

📦 [0.10.52] - September 5, 2026​

Changed​

  • [Issue #2430] Gate the brands, manufacturers, years and genres endpoints to taxonomy referenced by at least one visible set.
  • Customer-tier credentials now see roughly 30% of the previous brand and year lists; internal and read:all-status callers are unaffected. Set STATUS_GATE_TAXONOMY_ENABLED=false to restore the previous behaviour during an incident.

📦 [0.10.51] - September 5, 2026​

Added​

  • [Issue #2323] Record the credential type (api_key, pat, session) on each api_usage_logs row so portal-session traffic is separable from API-key traffic in analytics.
  • Portal traffic is still counted, not excluded; filter credential_type = 'api_key' to match the figure the portal dashboard reports.

📦 [0.10.50] - September 5, 2026​

Security​

  • [Issue #2433] Gate /v1/set-sources by the publication status of the owning set so draft sets' provenance records are no longer readable by customer-grade tokens.
  • Tokens holding write or delete with only read:published now receive a 404 instead of mutating or deleting a draft set's sources; internal and read:all-status tooling is unaffected.

📦 [0.10.49] - September 5, 2026​

Fixed​

  • [Issue #2479] Fix the Dependabot reviewer/assignee configuration so update PRs are actually assigned, and guard the config in CI.
  • The retired reviewers: key is deleted rather than re-slugged, so Dependabot PRs no longer carry a (already non-functional) review request; use .github/CODEOWNERS if one is wanted.

📦 [0.10.48] - September 4, 2026​

Fixed​

  • [Issue #2439] Declare the like: filter prefix in the OpenAPI spec, add a meta.scope signal and a ?scope=all enumeration parameter to GET /v1/sets, and escape % and _ in like: filter values.
  • Behavior change: ?name=like:% no longer returns the whole catalog — use ?scope=all instead.

📦 [0.10.47] - September 4, 2026​

Fixed​

  • [Issue #2468] Correct the v1 checklist OpenAPI description to list all five fields format=compact returns, not three.

📦 [0.10.46] - September 4, 2026​

Added​

  • [Issue #2290] Pin the usage endpoint's entitlement semantics with regression tests for non-entitling subscription statuses and revoked-key bucket isolation.
  • Test and documentation only; the underlying reporting defect was fixed by issue #2304 and the throttle-vs-report cross-checks by issue #2132.

📦 [0.10.45] - September 4, 2026​

Fixed​

  • [Issue #2147] Prove the opaque-data-key 403 on PATCH /v1/me with a feature test and correct the last pre-cutover sentence in the DELETE /v1/me OpenAPI description.
  • Documentation and test coverage only; no runtime behaviour change. The response-level annotation corrections shipped separately in #2478.
  • [Issue #2440] Scope the trial-reminder ledger to the trial window so extending a trial re-arms its reminders.
  • Adds billing_accounts.trial_reminders_window_ends_at; the migration backfills it from trial_ends_at for accounts that already hold a ledger, so no existing trial is re-notified on deploy.
  • [Issue #2504] Tolerate the legacy pageName query parameter on allowlisted v1 list endpoints, restoring /app/sets* on cardalmanac.com after strict query-parameter validation began rejecting it in 0.10.15.
  • pageName is accepted but deliberately left undocumented and out of rejection messages; it is a compatibility shim to be removed once cardtechie/tradingcardapi-sdk-php stops sending it.

📦 [0.10.44] - September 3, 2026​

Fixed​

  • [Issue #2438] Make format=compact on the v2 set checklist actually reduce response size by dropping the JSON:API envelope.
  • Compact v2 responses no longer emit included, per-card relationships, or per-resource links; pass an explicit include to opt back in.

📦 [0.10.43] - September 3, 2026​

Fixed​

  • [Issue #2374] Extend the published/draft status gate to card images so /v1/card-images list, show and download no longer expose images of cards under unpublished sets.
  • Reads of card images belonging to draft sets now 404 for tokens without read:draft; set STATUS_GATE_CASCADE_ENABLED=false to disable the whole cascade in an incident.

📦 [0.10.42] - September 3, 2026​

Fixed​

  • [Issue #2134] Correct the misleading 401/403 OpenAPI descriptions on the remaining interactive-session-gated endpoints, completing the #2342 sweep.

📦 [0.10.41] - September 3, 2026​

Fixed​

  • [Issue #2408] Fix CI on Dependabot pull requests by falling back to github.token when secrets.COMPOSER_TOKEN is unavailable, and exempt Dependabot from the changelog-fragment gate it cannot satisfy.
  • Dependabot runs use a separate secret store, so the empty COMPOSER_TOKEN made composer install fail with a 401 in both the Docker build and the phpcs job, blocking all 19 open Dependabot PRs.

📦 [0.10.40] - September 2, 2026​

Fixed​

  • [Issue #2472] Repair the master test suite: UserUsageTest asserted that GET /v1/user/usage was throttled on the anonymous api tier, which stopped being true when #2342 moved the route onto its own throttle:usage limiter, breaking Unit Tests on every open pull request.
  • Test-only. The assertion now references RouteServiceProvider::USAGE_RATE_LIMIT_PER_MINUTE instead of restating a tier the route no longer uses; the endpoint's behavior is unchanged.

📦 [0.10.39] - September 2, 2026​

Fixed​

  • [Issue #2424] Fail the release's documentation-dispatch step on a non-2xx response instead of printing success unconditionally, so an expired DOCS_UPDATE_TOKEN breaks the release visibly rather than silently stalling the public changelog.
  • The check tests the status as a 2xx class: POST /repos/{owner}/{repo}/dispatches returns 204, so an equality check against 200 would invert the bug into a false failure on every successful release.

📦 [0.10.38] - September 2, 2026​

Fixed​

  • [Issue #2342] Allow an opaque tc_ API key to read GET /v1/user/usage without consuming the quota it reports, and return an explanatory 403 instead of a bare 401 when a data key is refused by the interactive-session gate.
  • Behavior change: session.first_party routes that previously answered a data key with 401 now answer 403 FORBIDDEN_NOT_INTERACTIVE_SESSION; clients keying off the status code must be updated.
  • [Issue #2343] Emit X-RateLimit-Reset on every throttled response and expose the X-RateLimit-* headers through CORS so clients can derive their own usage window.
  • The throttle middleware alias now resolves to ThrottleRequestsWithReset; any environment overriding that alias must re-apply the change.

📦 [0.10.37] - September 2, 2026​

Added​

  • [Issue #2132] Add integration tests pinning the GET /v1/user/usage window against the limit throttle:api actually enforces for the caller's API key.

📦 [0.10.36] - September 2, 2026​

Added​

  • [Issue #2426] Add a CI guard requiring every data-mutating migration to document the production row count it was verified against.
  • The nine migrations that already mutate data are grandfathered by filename; new ones must carry @data-mutation and @production-impact tags.
  • [Issue #2427] Add brand and manufacturer filters to GET /v1/sets and expose the brand-to-sets relationship as an opt-in include=sets on /v1/brands.

📦 [0.10.35] - September 2, 2026​

Fixed​

  • [Issue #2425] Point the API's advertised changelog_url and the welcome-page GitHub link at public destinations instead of the private API repo.

📦 [0.10.34] - August 29, 2026​

Fixed​

  • [Issue #2362] Wire the Sentry DSN through the production deploy chain so unhandled exceptions are reported instead of dropped.
  • Requires a new PROD_SENTRY_DSN repository secret; until it is set the deploy prints the empty-optional-secret banner and error reporting stays inactive.

📦 [0.10.33] - August 29, 2026​

Security​

  • [Issue #2271] Bound the Loops and Brevo HTTP clients with explicit timeouts and queue the signup verification and welcome notifications so public signup endpoints no longer leak a registered-email timing oracle.
  • Deployed environments must set a non-sync QUEUE_DRIVER (not QUEUE_CONNECTION) with a running worker and a queue:prune-failed schedule, or the send stays on the request path and the oracle remains open.
  • LOOPS_TIMEOUT, LOOPS_CONNECT_TIMEOUT, BREVO_TIMEOUT and BREVO_CONNECT_TIMEOUT are optional repository variables (5s/2s defaults, supplied by prod compose); set one only to retune a bound during an incident, never to 0.
  • The configured timeouts are clamped to a finite, positive value before reaching the HTTP client, so an empty, zero, negative, non-numeric or overflowing setting (1e400 casts to INF, which Guzzle converts to an unbounded 0) falls back to the documented default instead of silently restoring an unbounded client.

📦 [0.10.32] - August 28, 2026​

Changed​

  • [Issue #2389] Note in the public API reference that the documented status scopes are not exhaustive and that read:all-status is the widest visibility available to an API customer.

📦 [0.10.31] - August 28, 2026​

Fixed​

  • [Issue #2388] Drop the trial-suspension-email index before its column in the migration's rollback so migrate:rollback no longer fails on SQLite.

📦 [0.10.30] - August 28, 2026​

Changed​

  • [Issue #2383] Record that /v1/players, /v1/teams and /v1/player-teams are intentionally not gated by publication status, declined on value rather than on cost, superseding the tracked plan to build a visibility signal.
  • No behaviour change: these endpoints already returned unfiltered taxonomy, and card data stays gated by #2347.

📦 [0.10.29] - August 28, 2026​

Fixed​

  • [Issue #2363] Wire BREVO_API_KEY through the production deploy path and report it as a readiness capability so founding-interest enrolment stops silently no-opping.
  • Requires a PROD_BREVO_API_KEY GitHub secret; until it is set the deploy logs a loud empty-secret warning and enrolment stays inactive.

📦 [0.10.28] - August 28, 2026​

Fixed​

  • [Issue #2360] Plumb FOUNDING_SLOT_CAP through the production deploy pipeline and add a FOUNDING_GATE_ENABLED flag, so the founding-slot cap can be raised or the gate turned off entirely without a code change.
  • Opt-in: set the PROD_FOUNDING_SLOT_CAP / PROD_FOUNDING_GATE_ENABLED repository variables to change the defaults; leaving them unset preserves the previous behaviour (cap 5, gate on).

📦 [0.10.27] - August 28, 2026​

Fixed​

  • [Issue #2406] Classify the ops-alerting config keys in the operator-tunable manifest, restoring a green master.
  • OPS_ALERTS_DRIVER is now pinned to slack in the production compose file so an operator can switch it to log or null without a release; OPS_SLACK_WEBHOOK_URL is declared runtime, and the stale OPS_ADMIN_EMAIL entry is removed.

📦 [0.10.26] - August 28, 2026​

Added​

  • [Issue #2364] Declare a documented 429 response on every throttled API operation and add reusable, example-bearing 429 and 422 response components to the OpenAPI spec.

📦 [0.10.25] - August 28, 2026​

Added​

  • [Issue #2361] Add a declared manifest of config keys and a CI guard asserting every operator-tunable key is actually reachable in production.
  • Adding an env() key to config/ now fails CI until it is classified in config/operator-tunable.php as runtime, baked, or ignored.

📦 [0.10.24] - August 28, 2026​

Fixed​

  • [Issue #2359] Route trial-lifecycle failure alerts, action summaries, and the weekly heartbeat through a driver-selected ops-alert seam with a Slack webhook driver, so they no longer terminate in a mail transport production does not have.
  • Removes OPS_ADMIN_EMAIL and config('mail.admin_address'); production must set OPS_SLACK_WEBHOOK_URL or the alerts fall back to error-level logs.

📦 [0.10.23] - August 27, 2026​

Security​

  • [Issue #2358] Pass GitHub Actions event data and secrets to workflow scripts through the step environment instead of splicing them into run: bodies, so payload text can no longer be parsed as shell.
  • Fixes deploys failing with syntax error near unexpected token when a deployment description contains parentheses or quotes.

📦 [0.10.22] - August 27, 2026​

Security​

  • [Issue #2347] Cascade published/draft status filtering from sets down to cards, so unpublished cards are no longer readable by customer credentials.
  • Breaking for consumers that relied on the unfiltered surface: default-scope /v1/cards totals drop sharply, and internal tooling must hold internal, read:draft or read:all-status to keep full visibility.
  • Players, teams and player/team pairings are not gated. Deriving their visibility from the cards they appear on was measured against production and rejected on performance grounds (see #2373); it needs a materialised visibility signal and is tracked separately. Their names remain readable for entities that appear only on unpublished cards.

📦 [0.10.21] - August 27, 2026​

Fixed​

  • [Issue #2076] Retry the trial-suspension email on a later sweep when the send fails, instead of losing it once the account leaves the trialing scope.
  • Adds a nullable trial_suspension_email_pending_at column to billing_accounts; run migrations on deploy.

📦 [0.10.20] - August 27, 2026​

Fixed​

  • [Issue #2123] Key the trial-reminder idempotency ledger on the stable days-remaining threshold instead of the derived "day N" label, so a trial-length or label-formula change can no longer re-send a reminder that already went out.
  • A one-time migration clears billing_accounts.trial_reminders_sent; pre-existing entries are label-keyed and are not reinterpretable as thresholds.

📦 [0.10.19] - August 25, 2026​

Fixed​

  • [Issue #2344] Report real published counts for players and teams on /v1/stats/counts instead of a hardcoded zero.

📦 [0.10.18] - August 22, 2026​

Changed​

  • [Issue #2340] Commit each release's CHANGELOG.md section straight to master instead of opening a chore/changelog-<version> PR nobody could merge.
  • The PR mechanism failed for an unforeseen reason: those PRs are markdown-only, and build-and-test.yaml skipped markdown-only PRs, so the required Unit Tests / Integration Tests contexts never reported and not one was ever mergeable. An unmerged changelog PR never lands its changelog.d/ fragment deletions, so each release re-collated its predecessor's fragments under its own version. Ten releases compounded that way, leaving master documented only through 0.10.7 while it shipped 0.10.17.
  • Backfills 0.10.8 through 0.10.17, each entry filed under the version that actually shipped it, verified against the tagged merge commits rather than inferred from the stacked PRs.
  • A release now fails loudly when a fragment survives collation, rather than silently deferring it to the next release — the failure mode that let this go unnoticed for ten releases.
  • Releases are serialized, and a release can no longer trigger another release.

📦 [0.10.17] - August 22, 2026​

Changed​

  • [Issue #2270] Document and test that a signup re-submit for an existing unverified account keeps the original password, and stop the success message from implying otherwise.
  • The submitted password is still discarded by design; updating it from an unauthenticated re-submit would let an attacker plant a credential that the victim's own verification click activates.

📦 [0.10.16] - August 22, 2026​

Fixed​

  • [Issue #2348] Accept the auto/* branch prefix in build/version.sh, so the sync PR that back-merge-stable-to-develop opens from auto/sync-master-to-develop no longer fails Version Script Validation and Bash Script Tests on its branch name alone.

📦 [0.10.15] - August 21, 2026​

Fixed​

  • [Issue #2305] Reject unrecognized query parameters on the v1 players, sets, and cards list endpoints with a 400 INVALID_PARAMETER error instead of silently ignoring them and returning the full collection.
  • Behavior change for callers: a request that previously returned 200 with unfiltered data now returns 400 naming the offending parameter.

📦 [0.10.14] - August 21, 2026​

Added​

  • [Issue #2303] Surface billing and lifecycle-email configuration gaps through an advisory checks.billing health key, a billing:doctor Artisan preflight, and a loud deploy-time warning.
  • Advisory only: a degraded billing config reports degraded and never changes the /health status code, so fail-soft deploy behavior is unchanged.

📦 [0.10.13] - August 21, 2026​

Fixed​

  • [Issue #2304] Report the dashboard's requests-used figure from the caller's API-key rate-limit bucket instead of the portal session's.

📦 [0.10.12] - August 21, 2026​

Fixed​

  • [Issue #2314] Add the back-merge-stable-to-develop workflow to master so its push-to-master trigger can actually fire, since push-triggered workflows are resolved from the pushed branch's own tree and the file previously existed only on develop.

📦 [0.10.11] - August 21, 2026​

Fixed​

  • [Issue #2307] Fix GET /v1/players/{id}/cards and filter[player_id] timing out at the default page size. Each stored morph form is now looked up on its own equality branch of the (on_cardable_type, on_cardable_id) index, and the result is fed to the card query as a materialized subquery. Measured against production for a player with 912 cards at per_page=25: 125s (timing out) to 0.86s for the row fetch, and 13.6s to 0.19s for the pagination count.

📦 [0.10.10] - August 21, 2026​

Added​

  • [Issue #2266] Enroll verify-time founding-waitlist losers into the Brevo founding interest list so they receive the "slots just opened" notification.
  • Requires a new BREVO_API_KEY in the API environment; with no key configured, enrollment is a reported no-op and the waitlist response is unaffected.

📦 [0.10.9] - August 20, 2026​

Fixed​

  • [Issue #2306] Fix a 500 on partial set-name search (GET /v1/sets?name=like:...) when a matching set's parent had been soft-deleted; the set now keeps its stored name instead of failing the whole listing.
  • The same null-parent guard now applies on the write path, so saving a set whose parent was soft-deleted no longer errors.

📦 [0.10.8] - August 19, 2026​

Fixed​

  • [Issue #2316] Fix changelog entries being filed under the wrong release. Pull requests no longer write a version number or date into CHANGELOG.md; fragments are collated once, at release, under the version actually shipping.
  • The release now delivers its collated changelog through a pull request instead of a direct push the branch ruleset refuses.

📦 [0.10.7] - August 19, 2026​

Fixed​

  • [Issue #2308] Fix the API landing page at https://api.tradingcardapi.com/, which advertised a base URL of https://api.tradingcardapi.com/api/v1 — a path that 404s. The versioned routes are mounted under API_PREFIX, which is empty in every deployed environment, so the correct base is https://api.tradingcardapi.com/v1. The page now derives the path from config('api.prefix') instead of hardcoding /api/v1, so it stays correct whether or not a prefix is configured. The wrong path had been live since at least 2026-08-10, covering the founding signup window, so anyone who started from the API root rather than the docs site got a 404 on their first request. The same stale /api/v1 path is also corrected in README.md, the architecture, testing and troubleshooting guides, and the feature PR template.

📦 [0.10.6] - August 18, 2026​

Fixed​

  • [Issue #2282] Address review findings on API-side invite enforcement.
  • A repeat POST /v1/signup for the same email is accepted again on a single-use invite. The gate previously rejected it with invite_exhausted before reaching the claim it already held, so an invitee whose verification email never arrived had no way back — the sign-up form refused them and POST /v1/signup/resend needs the token from the mail they did not get. The relaxation is scoped to the claim holder and to claims still inside their hold window; a different email, or a lapsed claim whose use has since been handed on, is still refused.
  • POST /v1/invites/validate now rejects a code that stops being usable between its verdict and the row read, instead of answering 200 valid with a null label for an invite POST /v1/signup would turn away.
  • POST /internal/invites accepts an explicit code: null as "mint a fresh code", matching the nullable the OpenAPI schema already advertised.
  • [Issue #2296] Declare the production signup throttle (RATE_LIMIT_SIGNUP = 60 requests per 10-minute window) in .docker/prod.docker-compose.yaml so the value raised for the launch survives deploys. It had only ever been set by hand on the production host, where build/deploy.sh rewrites the compose .env from scratch and re-copies the compose file on every deploy — silently reverting POST /v1/signup to the config default of 5 per 10 minutes per IP, which the whole marketing-site cohort shares because every signup arrives through the signup proxy's single egress IP.

Security​

  • [Issue #2282] Stop POST /v1/signup and POST /v1/signup/verify accepting credentials from the query string. Both validated and read through Request::input(), which unions the JSON body with the query string, so a request with an empty body and everything in the URL was accepted: POST /v1/signup?data[type]=signups&data[attributes][invite_code]=…&data[attributes][password]=… created an account, and POST /v1/signup/verify?data[attributes][token]=… completed a full verification — email verified, founding slot claimed, trial started, and the one-time tc_live_ API key minted. Live invite codes, user passwords and verification tokens were therefore recordable verbatim by any reverse proxy, CDN or WAF in the path, and travelled onward in Referer. Both endpoints now validate and read from the JSON body only, matching POST /v1/signup/resend, POST /v1/invites/validate and POST /internal/invites, which already did.

📦 [0.10.5] - August 16, 2026​

Security​

  • [Issue #2282] Require a valid invite code on POST /v1/signup so founding slots cannot be claimed by bypassing the marketing site's proxy gate.
  • Breaking for any existing caller of POST /v1/signup: data.attributes.invite_code is now required, and the shared cohort code must be seeded with php artisan invite:issue at deploy so links already in the August 18 cohort's hands keep working.

📦 [0.10.4] - August 15, 2026​

Fixed​

  • [Issue #2274] Fix the release pipeline, which had silently stopped producing releases and deploys after 0.10.0: the post-release step that commits the collated CHANGELOG back to master is rejected by the branch ruleset, and because it ran before the Docker build it took the image, tag, GitHub release, and the entire deploy job down with it.
  • Changelog fragments are now collated in the pull request rather than pushed to master after the merge, so the protected branch is never written to on the normal path.
  • The commit-back step now runs last and cannot fail a release; if it is rejected the release still ships and the run carries a warning.
  • Backfills the 0.10.1, 0.10.2 and 0.10.3 tags and releases for the three merges that were never released, and rebuilds their CHANGELOG sections from the curated fragments.

📦 [0.10.3] - August 15, 2026​

Added​

  • [Issue #2269] Add a public POST /v1/signup/resend endpoint so a user whose verification link has expired can request a new one from the /verify landing page, instead of going back through the sign-up form. The expired token is the only credential the page holds, so the endpoint is keyed on it rather than on an email address.
  • A resend rotates the verification token: the previous link stops working immediately and the new one carries a full 24-hour window. A double-submitted or retried resend is idempotent for 60 seconds, so a second click does not report the link it just replaced as unknown, and sends no second email.
  • The token must be sent in the JSON body; one supplied in the query string is ignored, since query strings are recorded by proxies and travel onward in Referer.
  • Non-enumerating — the 202 response is identical whether or not the account is already verified, and no email is sent when it is. A token that resolves to no account (never issued, already rotated, or consumed by a completed verification) is answered with 422 token_unknown.
  • Throttled per client IP and per verification token (3 and 2 requests per 10 minutes by default, tunable via RATE_LIMIT_SIGNUP_RESEND*), because every accepted call sends an email.

📦 [0.10.2] - August 14, 2026​

Fixed​

  • [Issue #2259] Mount helpers/ in the pre-PR gate and add it to phpcs.xml so helper code is validated from the branch rather than the container image's stale copy.

📦 [0.10.1] - August 13, 2026​

Added​

  • [Issue #2251] Add oauth:rotate-client-secret and oauth:client-metadata commands plus a rotation runbook so client-secret rotation preserves trust flags and names its consumers.
  • Rotation must now be done in place via the command; creating a replacement client silently drops is_internal / can_request_internal.

📦 [0.10.0] - July 19, 2026​

Added​

  • [Issue #2075] Add a customer-facing DELETE /v1/user/api-key endpoint that revokes the authenticated user's opaque tc_live_ API key without minting a replacement; returns 204 No Content, gated by the same interactive first-party session boundary as the view/regenerate routes, and an idempotent no-op when no live key exists.
  • [Issue #2179] Add the cleanup:fix-duplicate-tokens artisan command to detect and strip duplicate-adjacent manufacturer tokens from set display names; pass --dry-run for a classification-only preview, or run without flags to apply the idempotent fix.
  • [Issue #2067] Add a CI drift guard asserting every public (api.auth-exempt) route declares security: [] in the OpenAPI spec, so a new public endpoint cannot silently regress to a misleading Authorization header in the generated docs.
  • [Issue #1900] Add a billing:reap-orphaned-stripe-customers command that reports (and, with --delete, removes) Stripe customers orphaned by BillingAccount::forUser() concurrent-loser races.
  • Report-only by default and on the schedule; the destructive live delete runs only with the explicit --delete flag.
  • [Issue #2046] Add an operator runbook (docs/operations/loops-templates.md) specifying the five Loops transactional templates, the data variables each notification sends, and the per-environment provisioning checklist for LOOPS_API_KEY, PORTAL_URL, and the LOOPS_*_TEMPLATE_ID ids.
  • Also adds the missing LOOPS_CHANGE_EMAIL_TEMPLATE_ID to .env.example and indexes the operations runbooks in docs/README.md.
  • [Issue #792] Enforce per-plan API rate limits: a subscriber's opaque tc_ API key now carries the request tier for their plan — Trial and Pro at 10,000/24h, Starter at 1,000/24h — synced onto the key on trial start, on Stripe checkout, and on key regeneration, so the change takes effect immediately.
  • Tier values live in config('billing.plans') + the new config('billing.plan_rate_limits') (env-tunable); Enterprise/custom plans are set directly on the key and left untouched, and non-entitling statuses never zero a limit (suspension stays a separate access gate).
  • [Issue #1972] Mirror Stripe trial_end onto billing_accounts.trial_ends_at from the subscription webhook.
  • [Issue #1973] Add GET /v1/user/subscription returning the authenticated user's plan and trial state, degrading to a no-plan shape when the user has no billing account. Gated on an interactive first-party portal session (the same session.first_party boundary as the api-key endpoints), not the retired portal scope.
  • [Issue #1976] Add a per-environment Stripe billing-credentials runbook (docs/billing/stripe-credentials.md) documenting the required env vars and the POST /v1/billing/portal verification procedure.
  • [Issue #1978] Add PATCH /v1/user/email to change the login email with new-address re-verification before it takes effect.
  • Gated on an interactive first-party session (#1971); personal-access / opaque data keys are rejected.
  • [Issue #1979] Add DELETE /v1/me for authenticated first-party account and data deletion.
  • Irreversible; gated on session.first_party (PATs rejected) and requires explicit confirmation.
  • [Issue #1980] Add PATCH /v1/me so an interactive first-party session can update its own name and company.
  • [Issue #1981] Add PATCH /v1/user/password letting a portal user change their own password after verifying the current one.
  • Gated on an interactive first-party session (RequireInteractiveSession); data keys (PATs, opaque tc_ keys) are rejected. A successful change revokes the user's other tokens but keeps the current session.
  • [Issue #2012] Add a one-time billing:backfill-trial-ends-at artisan command that backfills trial_ends_at for existing trialing accounts from the Stripe API.
  • [Issue #2029] Plumb optional Stripe billing secrets (STRIPE_SECRET, STRIPE_WEBHOOK_SECRET, BILLING_RETURN_URL_ALLOWLIST) through the production deploy pipeline so the billing portal can be enabled per environment. Empty-tolerant: deploys never break when the secrets are unset; billing stays inactive until configured.
  • [Issue #2034] Add POST /v1/billing/checkout: creates a subscription-mode Stripe Checkout Session for the authenticated caller's billing account and returns the hosted URL, so the portal's Subscribe button can start self-service paid conversion. Gated on the interactive first-party session (like the billing portal, #2128); the plan selects the Stripe price by lookup_key (#2130); optional success_url/cancel_url are validated against the trusted-origin allowlist.
  • [Issue #2035] Add a public POST /v1/signup endpoint that creates an unverified user and dispatches a verification email, gated on founding-pool capacity.
  • Founding-pool enforcement and Loops email delivery are seams pending api#2040 / api#2037; until those land the capacity gate is always-open and email routes through the default notification channel.
  • [Issue #2036] Add POST /v1/signup/verify, the second step of self-service signup: it redeems the emailed verification token, atomically claims a founding slot, starts the 14-day app-managed trial (subscription_status = trialing, trial_ends_at), issues the single opaque tc_live_ API key (returned once), and sends the Loops welcome email.
  • Signup (#2035) minted a verification token but discarded it; the token is now persisted as a SHA-256 hash with a 24-hour expiry on the users table so it can be redeemed. A full founding pool returns a waitlist response and starts no trial; an expired token returns 410, an invalid/already-used token returns 422.
  • [Issue #2037] Add a Loops transactional-email driver and lifecycle notification classes (email verification, welcome, trial reminders, trial suspended).
  • Requires LOOPS_API_KEY and per-template ids per environment; marketing-list email stays on Brevo.
  • [Issue #2038] Suspend API access on data endpoints when a trial expires without converting, returning HTTP 402 until the subscription reactivates.
  • The API key is preserved; access auto-restores when the Stripe webhook flips the subscription back to active. Billing, subscription, api-key, and signup endpoints stay reachable while suspended.
  • [Issue #2039] Add a daily scheduled command that sends staged trial-reminder emails and transitions expired trials from trialing to canceled.
  • [Issue #2040] Add a config-capped founding-slot pool (remaining derived from trialing+active billing accounts) with race-safe acquisition, a one-trial-per-identity guard, and a public GET /v1/founding-slots/remaining endpoint.
  • Capacity defaults to FOUNDING_SLOT_CAP=5; slot release is automatic (derived count), so no webhook change is needed.
  • [Issue #2050] Add the Phase 0 opaque API-key design doc (ADR-005) freezing the tc_ key format, api_keys schema, and coupling-seam migration decisions.
  • [Issue #2051] Add the api_keys table, ApiKey model, and opaque-key generate/verify service as additive foundation for tc_ API keys.
  • Dead code until Phase 2 wires it into the request path; no route, middleware, or controller changes.
  • [Issue #2052] Authenticate opaque tc_ API keys alongside Passport tokens, with per-key rate limiting and last-used/usage tracking.
  • Additive only — existing PAT, password-grant, and client-credentials auth paths are unchanged.
  • [Issue #2062] Add Copilot instructions and decoy-migration warnings so reviewers stop suggesting a nullable/NULL OAuth user_id.
  • [Issue #2077] Add filter[player_id] to GET /v1/cards (and a GET /v1/players/{player}/cards convenience route) returning every card a player appears on, both directly and via a player-team.
  • [Issue #2078] Email a configured ops admin when trials:process-lifecycle sends reminders or suspends trials, and always on failure; opt-in via OPS_ADMIN_EMAIL.
  • [Issue #2108] Add an opt-in weekly heartbeat email for the trials:process-lifecycle sweep confirming the automation is alive during quiet stretches.
  • Opt-in via the existing OPS_ADMIN_EMAIL; adds an internal trial_lifecycle_runs ledger table (run php artisan migrate).
  • [Issue #2110] Add a feature test asserting the OpenAPI spec's 402 SUBSCRIPTION_REQUIRED coverage stays in sync with the subscription-gated route set (both directions).
  • [Issue #2127] Add GET /v1/user/usage returning the authenticated caller's current rate-limit window (limit, remaining, resets_at) as a JSON:API resource for the portal dashboard.
  • [Issue #2130] Add self-service checkout plan config (billing.plans): each plan slug (starter, pro) maps to a Stripe price lookup_key via env (STRIPE_PRICE_STARTER_LOOKUP_KEY / STRIPE_PRICE_PRO_LOOKUP_KEY), the single identifier that POST /v1/billing/checkout (#2034) and the subscription webhook's resolvePlan() agree on. Config prerequisite for self-service checkout.

Changed​

  • [Issue #2199] Upgrade intervention/image 3→4 (v4.2.0). Two v4 API removals affect the image-upload pipeline (ImageUploadService and the GenerateImageThumbnails job), each swapped for its v4 equivalent with no behavior change: $manager->read(…) → decodePath(…) / decodeBinary(…) (v4 replaced the generic reader with typed decoders), and $image->toJpeg($q) → $image->encode(new JpegEncoder(quality: $q)) (v4 removed the toJpeg() convenience wrapper). The re-encode-to-JPEG (metadata/polyglot stripping, #1488) behavior is preserved; orient, scaleDown, and width/height are unchanged in v4.
  • [Issue #2198] Upgrade stripe/stripe-php 20→21 (v21.0.0). The Stripe API version the SDK targets by default is unchanged between v20.3.x and v21.0.0 (2026-06-24.dahlia), and the app sets no explicit API-version override, so there are no billing-behavior changes; v21 is functionally a patch released as a major only because ErrorObject string properties were retyped to null|string for stricter type-checking. No application code changes were required.
  • [Issue #2196] Upgrade to Laravel 13 (laravel/framework 12→13, 13.20.0),
  • Also upgrades the test toolchain — phpunit/phpunit 12→13 and
  • [Issue #2196] Upgrade to Laravel 13. Bumps laravel/framework 12→13 (13.20.0) and the first-party/related packages whose ^12-only constraints would otherwise block resolution: laravel/tinker 2→3, darkaonline/l5-swagger 10→11, and spatie/laravel-activitylog 4→5. Pulls most Symfony components 7.4→8.1 transitively (a few, e.g. symfony/finder, remain on 7.4), which raises the effective PHP floor to 8.4.1 (Symfony 8 and PHPUnit 13 require >=8.4.1); composer.json is pinned to ^8.4.1 to match.
  • Also upgrades the test toolchain — phpunit/phpunit 12→13 (13.2.4) and brianium/paratest 7.13→7.23 — closing #2197, since Laravel 13 supports PHPUnit 13 and bundling avoids a redundant second dependency pass.
  • [Issue #2193] Refresh dependencies to their latest within-major versions: npm dev tooling (@stoplight/spectral-cli →6.16.1, @usebruno/cli →3.5.2, markdownlint-cli →0.49.1) and Composer packages (composer update — Stripe 20.3.1, slevomat 8.30.1, laravel-activitylog 4.12.3, passport, sentry, collision, paratest, etc.).
  • Available major upgrades (Laravel 13, PHPUnit 13, Stripe 21, Intervention 4, l5-swagger 11, Tinker 3, activitylog 5, js-yaml 5) are intentionally excluded and tracked as separate per-package upgrade issues.
  • [Issue #2163] Detect orphaned Stripe customers via a metadata.pending marker and a bounded Stripe search instead of scanning the full customer list.
  • Adds one off-lock, post-commit Stripe write on the first-billing path to clear the marker on a successful link; the --min-age-hours guard and the billing_accounts cross-check remain as defense-in-depth.
  • [Issue #1906] Run the project's bash test suites in CI via a new test-build-scripts runner and a Lint-workflow job. build/version.sh now resolves the branch from GitHub Actions' GITHUB_HEAD_REF/GITHUB_REF_NAME on a detached-HEAD (pull_request) checkout so the version-script suite runs there.
  • [Issue #1911] Assert the full JSON:API error envelope for the genres/deleted 404 regression tests instead of status-only.
  • [Issue #1909] Make the OpenAPI spec's per-operation security field authoritative by adding a top-level bearer-auth default and marking the public /health, /version, and /webhooks/stripe operations with an explicit empty security, so consumers can classify auth requirements directly from operation.security.
  • [Issue #2176] Split the staged trial-reminder email into three stage-specific Loops templates (7 days left / 2 days left / final day) selected by the reminder's days-remaining threshold, and add the portal URL to the payload so each reminder has a working billing CTA. Replaces the single LOOPS_TRIAL_REMINDER_TEMPLATE_ID with LOOPS_TRIAL_REMINDER_7DAY_TEMPLATE_ID, LOOPS_TRIAL_REMINDER_2DAY_TEMPLATE_ID, and LOOPS_TRIAL_REMINDER_FINAL_TEMPLATE_ID.
  • Necessary because Loops LMX is substitution-only and cannot branch email copy on a data variable; the prior single-template design also sent no CTA URL.
  • [Issue #2137] Consolidate the change-email confirm and signup verify token flows onto one shared EmailVerificationToken primitive (generate / SHA-256 hash-persist / 24h expiry / expiry check), replacing three hand-rolled copies with one audited source of truth. Internal refactor — no endpoint or token format change.
  • [Issue #1610] Ratchet the Bruno/OpenAPI drift CI check on a committed baseline, hard-failing only when drift grows above it.
  • Baseline starts at the current count (38); depends on #2005 to drive it to 0, at which point the ratchet becomes a strict hard-fail.
  • [Issue #1612] Stream sales-readiness update-cadence timestamps via COUNT/MAX aggregates and a window-bounded pluck instead of materializing the full publish history.
  • [Issue #1683] Unify the Deprecation response header on the RFC 9745 structured value @<unix-timestamp> across both emit paths by bringing AddDeprecationHeader in line with DeprecatedRoute.
  • The /internal/workflow/* legacy aliases now emit Deprecation: @<unix-timestamp> instead of Deprecation: true; internal consumers that parse the header value rather than its presence must treat the @<timestamp> structured form as the deprecation signal.
  • [Issue #1815] Log ParseChecklist overlap-lock skips instead of dropping them silently.
  • Behavior unchanged — overlapping jobs are still dropped (not released) to preserve the #1575 duplicate-card fix; the skip is now logged for observability.
  • [Issue #1824] Narrow in-repo test clients from the broad read:all-status scope to the least-privilege read:draft scope now that read:draft is grantable.
  • [Issue #1830] Remove the unreachable OVERRIDE_DEFAULT_STATUS override branch and its container binding, collapsing the default-status read path to the model property / PUBLISHED fallback.
  • [Issue #1883] Migrate the remaining V1 abort(404) sites in CardImageController and SetTodoController to the canonical JSON:API ApiException/ResourceNotFound error shape.
  • 404 bodies for these endpoints now carry a resource-specific code and the standard ResourceNotFound title/detail; clients that pinned the old abort()-rendered detail strings must update.
  • [Issue #1885] Synchronize QueryAwareCache key-registry writes under a cache lock on stores that support atomic locks, with a fallback for non-locking stores.
  • [Issue #1886] Normalize Genre and Attribute list-endpoint cache-key signatures so semantically-equivalent requests share one cache entry.
  • [Issue #1888] Extract the shared parallel-card copy logic into a ParallelCardCopier service so both parallel-set jobs delegate to one implementation.
  • [Issue #1892] Correct ADR-001's stale DeprecatePutUpdate / #1494 references to reflect #1646's outright PUT removal.
  • [Issue #1898] Commit the collated CHANGELOG, OpenAPI docs, README, and regenerated docs/API-REFERENCE-*.md back to master/develop after a release build so tag-only Dependabot releases get a documented section and consumed changelog.d/ fragments are actually removed.
  • The commit-back commit carries [skip ci] to avoid re-triggering its own build workflow.
  • [Issue #1971] Gate the customer API-key endpoints on an authenticated first-party portal session instead of the portal OAuth scope, and retire the portal scope.
  • Long-lived personal access tokens (data keys) can no longer reach /v1/user/api-key; portal clients should stop requesting the removed portal scope.
  • [Issue #1975] Document BILLING_RETURN_URL_ALLOWLIST and BILLING_DEFAULT_RETURN_URL in .env.example for the portal billing return-url flow.
  • Each environment must still set BILLING_RETURN_URL_ALLOWLIST to include the portal origin out-of-band; the example value is intentionally empty.
  • [Issue #1977] Extend GET /v1/me to return company plus plan, subscription status, and trial end from the user's billing account.
  • [Issue #2053] Cut customer API-key issuance over to opaque tc_live_ keys and reject tc_ data keys on the account-management endpoints.
  • Response shape change: key metadata now reports the opaque key_id as id and adds last_four; portal/SDK consumers need matching follow-ups.
  • [Issue #2054] Rewrite the API key docs around the opaque tc_ customer-key flow and drop stale PAT-as-key and "1 year" lifetime guidance.
  • [Issue #2065] Regenerate the parallel-test SQLite schema dump to include the founding_pool table and billing_accounts.has_trialed column.
  • The dump now carries the founding_pool id=1 sentinel seed row (a migration-inserted row schema:dump does not emit), so re-running the documented regeneration command must re-append it.
  • [Issue #2073] Derive trial-reminder day labels from a configurable trial length instead of hardcoding a 14-day trial.
  • Add TRIAL_LENGTH_DAYS (default 14); reminder send timing is unchanged.
  • [Issue #2084] Document the 402 SUBSCRIPTION_REQUIRED response on all subscription-gated v1/v2 data endpoints in the OpenAPI spec.
  • [Issue #2087] Stop accepting customer Passport personal access tokens on the public data path; opaque tc_ keys are now the only accepted customer data-key type.
  • Breaking: any client still authenticating /v1/* with a customer-minted PAT now receives 401 — first-party sessions and OAuth client-credentials are unaffected.
  • [Issue #2089] Derive the trial-reminder day-N label from each account's own trial window (trial_started_at → trial_ends_at) when a persisted trial start is available, falling back to the global billing.trial_length_days config otherwise.
  • [Issue #2114] Widen the trial-window backfill command so a one-time run also fills trial_started_at on already-populated trialing accounts.

Removed​

  • [Issue #2203] Remove the unused js-yaml direct dependency from docs/scripts. Nothing in the doc-generation/Bruno tooling imported it — the generators read the OpenAPI spec as JSON (public/docs/openapi.json), and the only real YAML consumer (openapi-markdown) bundles its own transitive js-yaml. Dropping the dead dependency (rather than bumping it to js-yaml 5) is functionally a no-op and ends the recurring Dependabot noise on it; supersedes and closes Dependabot PR #2105.
  • [Issue #2015] Remove the redundant build/update-openapi-version.php script and its invocations now that the OpenAPI info.version sentinel is pinned at the annotation source.

Fixed​

  • [Issue #1122] Confirm GET /v1/playerteams (and the canonical /v1/player-teams) returns a healthy paginated JSON:API collection instead of a 500, and add a regression test guarding the list path.
  • The original 500 was resolved earlier in the 0.10.0 cycle by the transformer eager-load work (#1551); this closes the tracking issue and locks in coverage for the previously-untested index action.
  • [Issue #2032] Correct the documented Docker Compose minimum to v2.24+, matching the long-form env_file object entry in docker-compose.yml.
  • [Issue #2178] Guard build/version.sh against a repo with no reachable tags: the no-tags git describe no longer aborts the script under set -e, so the 0.1.0-ci.N fallback works (protects shallow CI clones that fetch history but not tags).
  • Wired the comprehensive build/tests/test-version.sh scenario suite into the test-version / test-build-scripts CI runner after making it hermetic (throwaway repos outside the tree, trap-based cleanup, master default branch).
  • [Issue #2066] Fail closed in FoundingPoolService::tryAcquire() when the trial-start callback links no billing account, rolling back instead of returning Acquired.
  • [Issue #1907] Run the merge-conflict-marker guard on every PR by moving it into a standalone conflict-markers.yaml workflow, so PRs touching only non-paths-listed file types no longer skip the check.
  • [Issue #1902] Eager-load attributes and children on the card list endpoint when honored via ?include so they no longer trigger a per-card N+1.
  • [Issue #2152] Point the email-change confirmation link at the portal's confirm-email-change page instead of a non-existent API GET path, and make the confirmation send best-effort.
  • [Issue #1663] Stop coercing an empty string into the oauth_clients.user_id UUID column in UpdateVerbDeprecationTest; use a valid throwaway sentinel UUID for the first-party test client and correct the comment that misdescribed the schema as nullable. The migration/schema layer is intentionally left unchanged, since making the column nullable has historically broken OAuth.
  • [Issue #1816] Recurse the full subset tree on force-delete and clean up each subset's own sources and workflow/todos, not just immediate subsets' cards.
  • [Issue #1839] Remove the undocumented-in-schema start_year, end_year, and jersey_number attributes from the PlayerTeam API docs so the OpenAPI spec, reference, and Bruno collection match the actual create/update/response contract.
  • [Issue #1842] Pin the committed OpenAPI spec's info.version to a deterministic placeholder so regenerating the spec on any branch no longer produces spurious version churn.
  • The live API version is unaffected — it is reported by the /version endpoint and config('app.version'), not the committed spec.
  • [Issue #1849] Echo version-preview output before failing in version-preview.yaml so a non-zero version-preview exit surfaces its diagnostics instead of a bare exit code 1.
  • [Issue #1872] Make the api_player_teams de-dup migration prefer keeping a non-deleted row over a soft-deleted one for each player/team pair.
  • Edits the unreleased migration in place (it is on develop, not yet released); no new migration is added.
  • [Issue #1967] Auto-generate local-dev TLS certs from make up/upd/up-full/upd-full so a fresh clone's nginx no longer crash-loops on missing certs.
  • Also clears stale empty cert directories that Docker auto-created on previously-stuck clones, so the failure self-heals on the next make up.
  • [Issue #1968] Provision the local APP_KEY into a gitignored .env.local.secrets loaded via the compose env_file so POST /oauth/token no longer 500s locally.
  • Local-dev only; the generated key is now injected as a real container environment variable via the compose env_file, so it wins regardless of which dotenv file Laravel resolves under APP_ENV=local (previously it was written only to the shadowed .env, which Laravel never read).
  • [Issue #2005] Fix the Bruno↔OpenAPI sync tooling so bruno:sync drives the collection to zero drift non-destructively (prune stale requests, regenerate path-drifted ones, param-insensitive spec↔Bruno matching).
  • [Issue #2008] Stop the per-PR auto-version-bump from collating and deleting changelog.d/ fragments, so master/release/* PRs can pass the changelog-fragment gate; fragment collation now happens only at release.
  • [Issue #2028] Make the api_cards unique-live-key migration idempotent so a partially-applied schema no longer wedges php artisan migrate.
  • [Issue #2060] Render the public founding-slots endpoint without a misleading Authorization header in the generated API reference.
  • The docs generator now treats an explicit security: [] on an OpenAPI operation as unauthenticated, overriding the tag-based default.
  • [Issue #2061] Expand nested request-body examples in the generated API reference, and omit the Authorization header for public endpoints while adding Content-Type for JSON:API request bodies. Property-less object schemas now render as {} in generated examples instead of null, matching the schema and JSON:API expectations.
  • [Issue #2090] Clamp a misconfigured billing.trial_length_days back to the 14-day default so trial-reminder emails can no longer send negative day labels.
  • [Issue #2121] Enforce the single-live-key invariant on develop's opaque tc_live_ regenerate under concurrency via an atomic post-mint UPDATE.
  • [Issue #2128] Authorize POST /v1/billing/portal on the interactive first-party session instead of the write OAuth scope, so a least-privilege read:published portal session can open the Stripe billing portal.
  • [Issue #2151] Inline the request-body example as a -d payload in generated API-reference curl snippets so copy-pasted POST/PATCH examples no longer return 422.

Security​

  • [Issue #2193] Resolve all 13 open npm Dependabot security advisories in the build/docs tooling by bumping undici (→7.28.0), form-data (→4.0.6), markdown-it (→14.3.0), protobufjs (→7.6.5), and the transitive js-yaml copies (→3.15.0 / →4.2.0). npm audit is now clean in both / and /docs/scripts.
  • These are dev/build tooling dependencies excluded from the production image, so exposure was build-time only; composer audit reports no PHP advisories.
  • [Issue #2007] Add oauth:revoke-legacy-portal-keys command to revoke pre-#1970 portal-scoped customer API keys and their refresh tokens.
  • One-time remediation of already-issued keys; operators run it once post-deploy (supports --dry-run and --force).
  • [Issue #2048] Harden POST /v1/signup against timing-based email enumeration by holding the password-hashing cost constant across all branches.
  • Residual DB-insert/notification timing asymmetry is accepted and bounded by the route's existing throttling.
  • [Issue #2079] Bound the pre-auth rate limiter's unknown-token cache verdict by source IP so a flood of forged tokens can no longer evict cached known-token verdicts or generate unbounded DB lookups.
  • [Issue #2106] Gate the internal OAuth scope on per-client admin approval so only clients flagged is_internal can obtain or use it.
  • Deploy caveat: every existing is_internal=true client instantly gains internal-scope access; audit those rows and confirm the tradingcardapi-tools daemon client is approved (cardtechie/tradingcardapi-tools#1003) before rollout.
  • [Issue #2112] Split the internal OAuth scope gate onto a dedicated can_request_internal column so scope-trust is granted independently of the is_internal rate-limit pool. An operator can now grant the elevated rate-limit boost without also granting internal-scope access (and vice versa).
  • Existing approved clients are preserved on deploy (the migration backfills can_request_internal = is_internal); the column defaults to false, so a newly-created internal client must be explicitly granted can_request_internal — the rate-limit flag no longer implies internal-scope access.

📦 [0.9.9] - July 11, 2026​

Fixed​

  • [Issue #2120] Stop the per-PR auto-version-bump from writing a direct CHANGELOG/openapi version section onto master-targeting PR branches; those PRs now get a version-preview comment and rely on the changelog.d/ fragment plus post-merge collation.

📦 [0.9.8] - July 8, 2026​

Added​

  • docs: add changelog fragment for #1818

Changed​

  • test: exercise post-issue sweep with a raced PAT injected mid-issue (#1818)
  • docs: reconcile #1818/#1819 changelog dedup, drop conflict marker (#2115)

Fixed​

  • fix: resolve merge conflicts for PR #2115
  • fix: drop direct CHANGELOG/openapi version bump; keep changelog.d fragment (#1818)
  • fix: harden API-key regenerate against single-live-key race (#1818)

📦 [0.9.7] - July 7, 2026​

Added​

  • docs: add changelog fragment for #1818
  • docs: add changelog fragment for #1819

Fixed​

  • fix: drop direct CHANGELOG/openapi version bump; keep changelog.d fragment (#1818)
  • fix: harden API-key regenerate against single-live-key race (#1818)
  • fix: debounce track.token.used last-used stamp (#1819)

📦 [0.9.6] - June 28, 2026​

Added​

  • docs: add changelog fragment for #1974
  • feat: add GET /v1/user/usage rate-limit window endpoint (#1974)

Fixed​

  • fix: add constructor docblock short description for #1974 (phpcs)

📦 [0.9.5] - June 27, 2026​

Added​

  • docs: add changelog fragment for #1971

Fixed​

  • fix: gate /v1/user/api-key on interactive session, retire portal scope (#1971)

📦 [0.9.4] - June 27, 2026​

Added​

  • docs: add changelog fragment for #2008

Fixed​

  • fix: skip changelog.d collation in per-PR version bump (#2008)

📦 [0.9.3] - June 27, 2026​

Security​

  • [Issue #1970] Issue the customer/portal API key with the read:published data scope instead of the management portal scope, so a data key can no longer read or rotate its owner's key.
  • Security fix: pre-existing portal-api-key PATs still carry portal and remain an escalation vector until rotated; operators should regenerate affected keys after deploy.

📦 [0.9.2] - June 24, 2026​

Fixed​

  • [Issue #1962] Restore the original flat { id, name, email } payload on the deprecated GET /user alias, which #1502 had silently changed to the JSON:API shape — breaking consumers (e.g. the admin login) that read the user fields at the top level. GET /v1/me keeps the new JSON:API resource; the /user alias keeps its deprecation/Sunset headers. A deprecated alias must preserve its response contract, not change shape.

📦 [0.9.1] - June 23, 2026​

Fixed​

  • [Issue #1952] Revert the #1540 number_int generated sort column, which crashed production migrations on non-numeric card numbers (e.g. SD38) under strict MySQL sql_mode (a STORED generated CAST(... AS UNSIGNED) is a write, which strict mode rejects). The set-checklist sort returns to the read-time orderByRaw('CAST(number AS UNSIGNED) ASC, number ASC') ordering, which is unaffected by strict mode. A proper index-backed natural sort is tracked separately in #1951.

📦 [0.9.0] - June 23, 2026​

Added​

  • [Issue #1599] Expose per-card has_player/has_team flags in the V1 and V2 set checklist endpoints via bulk subqueries.
  • Flags appear in both full and compact formats; non-checklist CardTransformer consumers default the flags to false when the columns are not selected.
  • [Issue #1767] Add GitHub Actions buildx layer caching to the Build Docker Image CI job so unchanged toolchain layers are reused across runs.
  • Uses type=gha,mode=max; the GHA cache has a ~10 GB per-repo eviction budget — if churn defeats the cache, fall back to mode=min or a registry cache.
  • [Issue #1759] Add parallel PHPUnit execution via paratest with a make test-parallel target and per-worker isolated in-memory SQLite.
  • PCOV now defaults to disabled in the image; coverage runs must pass -d pcov.enabled=1 to the PHP interpreter — e.g. php -d pcov.enabled=1 vendor/bin/phpunit … (the make test-coverage* targets already do).
  • [Issue #1505] Add OpenAPI annotations and Bruno requests for the PlayerTeams update and delete endpoints.
  • [Issue #1472] Escape DB credentials with escapeshellarg() in DatasetBackupCommand to close a shell-injection vector in the mysqldump exec path.
  • [Issue #1465] Remove the hardcoded Mailgun API key and unused Mailgun mail config from .docker/docker-compose.full.yml and config/services.php; the application sends no email and the Mailgun transport was never installed.
  • The leaked key must still be rotated in the Mailgun dashboard and scrubbed from git history out-of-band; this change alone does not remove it from history.
  • [Issue #1492] Paginate GET /v1/card-images (default 50 per page, per_page clamped to 100) instead of returning every row.
  • Response shape now includes pagination links/meta; clients that consumed the full unbounded list must page through results.
  • [Issue #1516] Populate source.pointer (RFC 6901 JSON Pointer) on every 422 validation error so clients can map errors back to the offending request field.
  • Validation errors are now emitted per-attribute via errors()->messages() instead of flattened via errors()->all(), which can change the count/order of error objects in a 422 response.
  • [Issue #1471] Configure explicit Passport token expiries (8h access, 30d refresh, 6mo PAT) instead of Passport's 1-year default.
  • Tokens issued before this change keep their original 1-year expiry until reissued; shorten by revoking and reissuing if needed.
  • [Issue #1455] Add meta.full_total to GET /v1/workflow/actionable-sets reporting the unpaginated count of matching rows.
  • Additive — meta.total is unchanged (still the returned page size); downstream admin/SDK changes are separate follow-ups.
  • [Issue #1443] Expose analyze:sales-readiness as an authenticated internal HTTP endpoint (GET /internal/analytics/sales-readiness) supporting format=json and format=table.
  • [Issue #1440] Add an Update Cadence section to analyze:sales-readiness reporting last publish date, days since last publish, monthly publish trend, and average publishes per month.
  • [Issue #1439] Add pipeline-throughput metrics (per-step average duration, completion trend, oldest in-flight set, backlog ETA) to the analyze:sales-readiness workflow status section.
  • [Issue #1493] Add a public, unauthenticated GET /health endpoint that checks database and cache connectivity and returns 200 when healthy or 503 when a dependency is down.
  • [Issue #1520] Add Handler::getErrorResponse() branches for validation, 404, 405, generic HTTP, throttle, authorization, and unknown exceptions so API errors no longer return an empty errors array.
  • meta.request_id is attached only when the request.id binding from #1517 is present; until #1517 lands the field is omitted.
  • [Issue #1495] Add v1/player-teams as the canonical kebab-case path for player-team associations, keeping v1/playerteams as a deprecated alias.
  • Breaking-change scope: the JSON:API type and internal route name stay playerteams; only the URL path changes, and the old path is preserved as an alias.
  • [Issue #1517] Add request-ID correlation: an X-Request-ID response header on every response, request_id in log context, and meta.request_id on JSON:API error bodies.
  • [Issue #1525] Add an assertJsonApiErrorShape() test helper and retrofit error-path assertion sites to enforce the JSON:API error contract (non-empty errors[], string status, title/detail, conditional source pointer/parameter, no APP_DEBUG leakage).
  • [Issue #1534] Cache the manufacturers, brands, and years list endpoints with model-observer invalidation on save/delete.
  • Genres and attributes are excluded — their list endpoints are paginated/filtered and need a query-aware caching approach tracked separately.
  • [Issue #1545] Add ADR-004 documenting the single-droplet production infrastructure posture — keep database cache/queue plus host-volume storage for launch, and defer Redis and DigitalOcean Spaces object storage with explicit revisit triggers.
  • [Issue #1546] Add an X-Request-ID correlation middleware (reads an inbound header or generates a UUID, binds it to the container, and echoes it on the response) plus a Monolog processor that injects request_id into every log record across the local, CI, and production log channels.
  • [Issue #1548] Centralize API error handling: add Factory::error(), replace the reflective ModelNotFoundException handler with a mapped exception-type registry, and add branches for the standard Symfony/Laravel exception types.
  • Unmapped models now return a generic JSON:API 404 instead of a fatal 500.
  • [Issue #1553] Document the API versioning and deprecation policy as an ADR.
  • [Issue #1554] Document the UUIDv4-vs-UUIDv7 primary-key strategy in a new architecture decision record.
  • [Issue #1555] Add a CI guardrail that fails PRs introducing unpinned GitHub Actions references.
  • [Issue #1560] Add a dedicated higher-quota rate-limit pool for internal callers, tunable via RATE_LIMIT_INTERNAL / RATE_LIMIT_INTERNAL_WINDOW, granted only through a DB-trusted is_internal client flag so public paid keys keep their existing quota.
  • Client-keyed and independent of #1557 (internal scope + /internal/* routes); the pool applies regardless of route placement or scope gating.
  • [Issue #1566] Add a billing_accounts table and users.billing_account_id linkage with a lazy BillingAccount::forUser() helper that attaches a Stripe customer on first billing action.
  • Foundation only — no subscription/plan/period state; all billing code must go through forUser() rather than reading stripe_customer_id directly.
  • [Issue #1567] Add an authenticated POST /v1/billing/portal endpoint that opens a Stripe Customer Portal session for the caller and returns its URL.
  • New config/billing.php return_url allowlist must be populated; an empty allowlist rejects every caller-supplied return_url.
  • [Issue #1568] Add a signature-verified POST /webhooks/stripe endpoint with an idempotent Stripe event log.
  • Requires STRIPE_WEBHOOK_SECRET to be set; event handlers are log-only stubs (real handling lands in #4).
  • [Issue #1569] Mirror Stripe subscription state onto billing_accounts via webhooks and add the canonical isSubscribed() gate.
  • [Issue #1596] Document the portal password-grant OAuth client and its least-privilege read:published end-user scope, with a feature test covering token issuance and /user lookup.
  • [Issue #1598] Add authenticated customer API-key endpoints: GET /v1/user/api-key returns masked key metadata and POST /v1/user/api-key regenerates the key, returning the plaintext exactly once and invalidating the prior key immediately.
  • The customer "API key" is a single per-user Passport Personal Access Token; adds a last_used_at column to oauth_access_tokens (run migrations).
  • [Issue #1611] Add unit tests for the pure helpers in the Bruno-sync scripts (import-openapi.js filename/version routing and validate-collection.js structure checks), wired into docs/scripts npm test.
  • [Issue #1615] Add a lightweight count-only GET /internal/actionable-sets/count endpoint returning the unpaginated actionable-set count without materializing the row page.
  • [Issue #1661] Implement the PlayerTeamController update and destroy actions so PATCH/PUT and DELETE on player-team associations perform work instead of no-opping.
  • [Issue #1670] Add a .claude/pre-pr-gate.md declaring the workspace-safe Docker PHPUnit + phpcs gate so the picklewagon-mcp runner's pre-PR gate runs against the workspace clone.
  • [Issue #1705] Add an oauth:set-rate-limit artisan command that updates a client's rate limit and immediately invalidates the cached per-token lookup so changes apply at once instead of after the 10-minute TTL.
  • [Issue #1709] Provision a redis:7-alpine service in the local Docker stack and add the predis/predis client so the existing redis cache/queue store is usable in development.
  • Local-only; production provisioning and any default-store change are tracked separately. The default CACHE_DRIVER stays database.
  • [Issue #1716] Cache the paginated GET /v1/genres and GET /v1/attributes list endpoints (1h TTL) with query-aware cache keys and model-observer invalidation on save/delete.
  • Invalidation uses a per-resource key registry rather than cache tags, so it works on the non-taggable default store; AttributeController::update() was switched off a query-builder mass update so the observer fires.
  • [Issue #1730] Add idx_cards_deleted_at on api_cards(deleted_at) so the windowed stats deletions aggregate is index-backed on cache miss, mirroring idx_cards_created_at for additions.
  • [Issue #1746] Add external error reporting via a driver-based ErrorReporter seam (default sentry, DSN-gated so it is a no-op without SENTRY_LARAVEL_DSN; set ERROR_REPORTER_DRIVER=null to disable).
  • The exception handler forwards through the seam only for exceptions it would otherwise log (honouring shouldReport()), tagging each event with the request correlation id from #1546; request-body capture and performance/APM tracing are disabled (the latter deferred to #1527).
  • [Issue #1754] Add regression tests asserting meta.request_id is present on 422/429/404/405/403 JSON:API error responses.
  • [Issue #1755] Add a CI gate that fails non-trivial PRs lacking a changelog.d/ fragment, with a skip-changelog label escape hatch.
  • Opt out of the gate by adding the skip-changelog label to the PR.
  • [Issue #1763] Add a periodic code-coverage workflow (coverage.yaml) that runs on merge to develop/master (and on demand), publishing the overall line-coverage percentage to the run summary and archiving the clover report as an artifact. It is non-blocking — coverage never runs on, slows down, or gates pull requests.
  • [Issue #1782] Add a CI guard that fails the lint workflow when committed files contain merge-conflict markers or the custom sentinel comments a botched AI-assisted merge resolver leaves behind in place of a real resolution.
  • [Issue #1790] Add a live-SDK wiring test for StripeCustomerService and refresh its docblocks now that stripe/stripe-php is installed.
  • [Issue #1808] Persist Stripe invoices from invoice.paid / invoice.payment_failed webhooks into a local invoices table instead of log-only.

Changed​

  • [Issue #1766] Run the CI unit-test coverage job through paratest to cut its wall-clock.
  • [Issue #1503] Attach the json.api.headers middleware to the v2 route group so the v2 set-checklist endpoint negotiates JSON:API content types like v1.
  • [Issue #1438] Add a base set completeness section to analyze:sales-readiness comparing each published base set's card_count against actual cards across its subsets.
  • [Issue #1572] Confirm and document that PATCH /v1/set-todos/{id} accepts status=pending from any state (including review); add regression tests and add review to the OpenAPI status enums.
  • [Issue #1441] Add an Issue Creation section to .claude/CLAUDE.md directing Claude to use the create_cross_repo_issues MCP tool instead of raw gh issue create.
  • [Issue #1456] Confirm and document that GET /v1/workflow/actionable-sets?step=audit&status=in_progress surfaces in-progress audits for rediscovery; add a named regression test pinning that contract.
  • The user-facing dashboard fix (an "In Progress" filter option) lives in tradingcardapi-admin#1319; no API behavior change.
  • [Issue #1447] Resync the Bruno collection with the OpenAPI spec and clear missing-test and missing-auth validation warnings.
  • Makes npm run bruno:sync idempotent so future PRs start from a clean git status.
  • [Issue #1502] Move the GET /user closure to a dedicated GET /v1/me controller and keep /user as a deprecated alias.
  • /user now emits a Deprecation header pointing to /v1/me; the alias is slated for removal in v2.
  • [Issue #1486] Add the https:// scheme to APP_URL in the production Docker compose file so Laravel builds correct absolute, secure URLs.
  • [Issue #1501] Consolidate the v1/workflow/actionable-sets and v1/workflow/bulk-initialize[/{job_id}] endpoints onto RESTful paths (v1/actionable-sets, v1/todo-initialization-jobs) and keep the legacy v1/workflow/* URLs as deprecated aliases that emit a Deprecation header.
  • Breaking on removal: the legacy v1/workflow/* aliases are slated for deletion in v2; migrate clients to the new paths before then.
  • [Issue #1500] Move POST v1/sets/{id}/sync-parallel to the JSON:API action namespace POST v1/sets/{id}/actions/sync-parallel, keeping the old path as a deprecated alias that emits deprecation headers (Deprecation/Sunset/Link/Warning).
  • Breaking once the alias is removed in a future release; consumers should migrate to the action route during the deprecation window.
  • [Issue #1499] Replace the orphan PATCH /v1/set-todos/{id} and GET /v1/workflow/sets/{id}/todos with a full nested v1/sets/{set}/todos resource (index/show/store/update/destroy).
  • Breaking: the old set-todos/{id} and workflow/sets/{id}/todos paths are removed; clients must move to the nested routes.
  • [Issue #1497] Express v1 genre soft-delete state via filter[status]=deleted and include_trashed=true query parameters instead of literal deleted URL segments; keep the old routes as Deprecation-headed aliases.
  • Breaking for v1 consumers at v2 when the literal-segment aliases are removed.
  • [Issue #1498] Promote the bulk card-image endpoints to a first-class v1/card-image-batches resource; keep v1/card-images/bulk as deprecated aliases (RFC 9745 headers) slated for removal in v2.
  • Breaking for direct consumers of the old URLs once the aliases are removed in v2.
  • [Issue #1496] Rename {id} route parameters to named resource bindings ({set}, {card_image}) on the set-checklist, set-workflow, card-image-download, and v2 set-checklist custom-action routes, converting their controllers to route-model binding.
  • Non-breaking for clients: URL patterns are unchanged; the change is internal route-model resolution.
  • Reorder the api middleware group so status.ignore runs before bindings, ensuring implicit {set} bindings respect the caller's status scope (draft sets stay hidden from published-only callers); the workflow endpoint keeps its deliberate unscoped set lookup.
  • Fix the exception handler so converted ApiExceptions render in place instead of escaping the middleware pipeline; error responses (e.g. 401s) now consistently carry outer-middleware headers such as X-RateLimit-* regardless of middleware order.
  • [Issue #1494] Make PATCH the canonical update verb for all v1 apiResource endpoints per JSON:API §2.5 and emit a Deprecation header on PUT updates.
  • PUT remains a working alias through a deprecation window; it is slated for removal in the next major version, which will be a breaking change for PUT-based SDK callers.
  • [Issue #1485] Generate local-dev TLS certs on demand via .docker/scripts/generate-dev-certs.sh (mkcert for developers, openssl fallback for CI) instead of committing *.dev private keys.
  • Run bash .docker/scripts/generate-dev-certs.sh before docker compose up after pulling this change; the keys are no longer in the repo.
  • [Issue #1542] Pass checklist response format to the set-checklist transformers via constructor injection instead of a dynamic model property.
  • [Issue #1507] Replace the fabricated Legacy: GET /cards migration example in the public API reference template with the real GET /v1/stats/{modelType} → GET /v1/stats/counts migration, and regenerate the rendered doc.
  • [Issue #1475] Scope CORS away from wildcard origins to an env-driven CORS_ALLOWED_ORIGINS allowlist with explicit paths, methods, and headers.
  • Deploy prerequisite: set CORS_ALLOWED_ORIGINS in each environment, or browser origins will be denied.
  • [Issue #1478] Wrap non-unique-constraint QueryExceptions in a 500 with a logged correlation ID so DB internals never reach API clients.
  • [Issue #1480] Route Card/Player/Team V1 controller updates through Eloquent fill()->save() so $fillable blocks mass assignment of non-fillable columns (e.g. deleted_at, created_at).
  • [Issue #1491] Drop internal FK UUIDs (genre_id, manufacturer_id, brand_id, year_id) from flat Set attributes; they remain available via ?include=genre,manufacturer,brand,year relationships.
  • Consumer-visible response change — clients reading these flat attributes must switch to the include-based relationships.
  • [Issue #1506] Align update-endpoint OpenAPI annotations to PATCH across 11 V1 controllers and regenerate the spec.
  • [Issue #1509] Normalize remaining Bruno v1 request files to the no-parentheses naming convention and guard against regressions.
  • Out of scope: Card Images/ scenario files keep their descriptive names; no bru run behavior changes.
  • [Issue #1510] Harmonize the stats route parameter on {modelType} to match the controller signature, OpenAPI spec, and internal docs.
  • [Issue #1518] Add canonical Factory::error() / Factory::errors() helpers and route the 9 hand-rolled SetController error responses through them.
  • [Issue #1523] Detect unique-constraint violations per database driver (SQLite 23000/19 narrowed by the "UNIQUE constraint failed" message, MySQL 23000/1062, PostgreSQL 23505) in the exception handler, so non-unique integrity errors (FK / NOT NULL / CHECK) are no longer mapped to 409 Conflict or suppressed from reporting.
  • [Issue #1524] Scrub user-controlled detail from ApiException::getErrors() DEBUG-log context so user input never reaches persistent logs.
  • [Issue #1527] Skip api.logging middleware's LogApiUsage dispatch when the queue driver is sync, eliminating a synchronous per-request insert into api_usage_logs.
  • [Issue #1528] Default the cache store to database instead of file so rate-limit buckets and cached reads are shared across containers.
  • Deployments relying on the old file default without setting CACHE_DRIVER now require the cache and cache_locks tables (existing migrations) to be migrated.
  • [Issue #1529] Eager-load year, manufacturer, brand, and parent in SetController::index to remove the per-set N+1 that Set::getNameAttribute triggered on the /v1/sets list endpoint.
  • [Issue #1531] Eager-load subset/card counts on the parentset and subsets includes so SetTransformer no longer fires a COUNT query per rendered set.
  • [Issue #1532] Auto-queue POST /v1/sets/{id}/checklist imports above 50 lines so large checklists no longer block the request thread; queue=true/queue=false still force async/sync.
  • [Issue #1533] Default the parallel-set sync endpoint to async dispatch and force queueing when the parent set has more than 20 cards.
  • The queue query param now defaults to auto; omitting it on a large parent set changes the response from a synchronous 200 to a 202 Accepted.
  • [Issue #1535] Bounded the GET /v1/stats/{type} aggregate queries to a rolling one-year window and added idx_cards_created_at so cache-miss stats no longer full-scan the entire table.
  • Stats now cover the last 12 months only; older history is no longer returned by this endpoint.
  • [Issue #1536] Stream a set's cards with cursor() in the Set deleting hook to bound peak memory, preserving per-card cascade side-effects.
  • [Issue #1537] Paginate GET /v1/set-sources with a default page size of 50 and a 200 max cap to bound the previously unbounded get().
  • Response now includes pagination meta/links; clients reading the full list must page through results.
  • [Issue #1539] Drop the whereRaw('1 = 1') query seed in the V1 Player and Team index endpoints in favor of Model::query() and a self-contained filter builder, preserving exact-match and full-name split behavior.
  • [Issue #1540] Replace the CAST-based checklist sort with a generated number_int column and composite index to eliminate the filesort on set-checklist queries.
  • Adds a MySQL generated stored column; the migration is driver-aware so the SQLite test database uses an INTEGER-cast equivalent.
  • [Issue #1541] Eager-load the set relation in the V1 checklist and drop Card::getSetAttribute's per-call shared-cache path.
  • [Issue #1543] Document why CardController::update() saves field_overrides via the instance fill()->save() path so the Card array cast applies (and a query-builder update() must not be substituted, as it bypasses both $casts and $fillable); no behavior change.
  • [Issue #1544] Skip the per-card parallel-subset query in CardObserver by caching a has-parallel-subsets boolean on the Set model.
  • [Issue #1547] Reduce api_usage_logs write amplification by dropping two of its four composite indexes and add a daily logs:prune-api-usage command that deletes rows older than 30 days.
  • The dropped indexes (idx_endpoint_time_performance, idx_time_status_errors) are removed by migration; roll back the migration to restore them.
  • [Issue #1549] Add failed() dead-letter handlers to all queue jobs and idempotency guards to the parallel-set write jobs, so retried jobs do not double-apply and exhausted jobs log with correlation context.
  • Guardrails only; the Bus::batch() / domain-event-bus refactor is deferred to a follow-up.
  • [Issue #1550] Extract StatsSnapshotController snapshots/growth validation into FormRequests and move aggregation/transform into EntityStatsService, thinning the controller.
  • Validation-error responses on /v1/stats/snapshots and /v1/stats/growth now use the JSON:API errors[] shape (was {message,errors}); also resolves #1515.
  • [Issue #1551] Make transformer eager-load requirements explicit via a DeclaresEagerLoads contract so controllers load relations deliberately instead of relying on hidden defaultIncludes, and wire the contract into the V1 player-team and V2 set checklist endpoints.
  • [Issue #1558] Classify workflow, audit-log, and set-todo endpoints as internal so they render in the internal API reference instead of the public one.
  • [Issue #1559] Move the internal-only workflow, set-todos, and audit-log requests into a docs/bruno/internal/ folder so the public Bruno collection no longer exposes them.
  • [Issue #1606] Render the internal sales-readiness table from the cached report array instead of re-running the artisan command, so json and table responses share one computation.
  • [Issue #1607] Anchor the analyze:sales-readiness oldest-in-flight metric on the current-step todo's started_at (falling back to workflow created_at) so it reflects the longest current-step wait rather than total workflow age.
  • [Issue #1608] Stream buildThroughputMetrics() completed-todo reads with ->cursor() and compute the per-step and publish-trend aggregates in a single pass to bound memory on large datasets.
  • [Issue #1642] Audit downstream consumers for reads of the flat Set FK attributes removed in #1491 and file per-repo follow-ups for any real dependency.
  • [Issue #1675] Reword the inaccurate "Legacy: Direct endpoints (deprecated)" line in the public API reference's API Versioning section to describe the real legacy surface (/version and /v2/sets/{set}/checklist).
  • [Issue #1678] Normalize the v2 Bruno Sets checklist request files to drop their parenthesized qualifiers.
  • [Issue #1686] Assert the exact JSON-Pointer value for a nested validation attribute in the 422 source.pointer test.
  • [Issue #1698] Prevent duplicate PlayerTeam rows via a composite unique constraint on (player_id, team_id) and dedup-aware write paths.
  • Migration de-duplicates existing rows (keeps oldest per pair) before adding the constraint; rollback's loss of those duplicate rows is intentional since duplicates were always invalid.
  • [Issue #1703] Move ApiException diagnostic DEBUG logging out of getErrors() into the centralized Handler error path, preserving the #1524 detail-key scrub.
  • [Issue #1710] Route the legacy genres/deleted alias through the DeprecatedRoute middleware so the deprecation header rides every response the route produces, not just 2xx.
  • The Deprecation header value changes from the literal true to the RFC 9745 @<timestamp> form, and Sunset/Link/Warning headers are now emitted alongside it.
  • [Issue #1717] Derive the StatsController security test's allowlist from the controller constant instead of a hardcoded copy.
  • [Issue #1727] Batch parallel-card creation into a single job per parallel subset (array of card ids) instead of one job per card-subset pair, cutting dispatch fan-out during bulk imports.
  • [Issue #1728] Normalize the DeprecatedRoute sunset-override to end-of-day GMT so a bare-date override emits a stable, timezone-independent Sunset header.
  • [Issue #1729] Emit source.parameter instead of source.pointer on 422 validation errors for query-param-validating requests (actionable-sets, DELETE .../checklist, stats/snapshots, stats/growth), per JSON:API §7.1.3.
  • [Issue #1751] Introduce a driver-based UsageMetricsRecorder seam for API-usage metrics and record the off-MySQL migration decision in ADR-003.
  • Defaults to the existing MySQL database driver — no behavior change until an operator sets API_USAGE_METRICS_DRIVER; selecting a real time-series backend is deferred to a follow-up.
  • [Issue #1761] Standardize the GET /v1/stats/{type} 404 to the canonical JSON:API error shape via ApiException/ResourceNotFound.
  • The 404 body now includes code, detail, and meta.request_id, and its title changes from "Model type not found" to the generic resource-not-found title — clients pinning the exact body must relax to structure assertions.
  • [Issue #1769] Update the workflows README to describe the Unit Tests with Coverage job as paratest-based parallel coverage (WrapperRunner, PCOV re-enabled per worker) instead of serial PHPUnit.
  • [Issue #1772] Remove PCOV coverage instrumentation from the PR unit-tests CI job to take coverage off the PR critical path.
  • Coverage is reintroduced deliberately via #1763 (PCOV stays disabled-by-default in the image), and the always-on <coverage> report block is dropped from phpunit.xml so paratest no longer requires a coverage driver on every run; coverage stays opt-in via CLI flags (e.g. build/scripts/coverage-report).
  • [Issue #1774] Adopt changelog.d/ fragments and collate them into CHANGELOG.md at release time so concurrent PRs stop conflicting on the shared ## [Unreleased] section.
  • PRs now ship a changelog.d/<issue>-<type>.md fragment instead of editing CHANGELOG.md; fragments are excluded from Markdown Lint and assembled at release.
  • [Issue #1777] Include pending changelog.d/ fragments in make changelog-preview so the dev-facing preview reflects unreleased changes that have not yet been collated into CHANGELOG.md.
  • Read-only: the preview collates against temp copies and never mutates CHANGELOG.md or deletes fragments.
  • [Issue #1780] Commit an ADR-001 Sunset window on the deprecated GET /user alias and document the field-level deprecated: true Swagger convention.
  • [Issue #1801] Move the Stripe customer creation in BillingAccount::forUser() outside the DB transaction so no row lock is held during the external call, preserving exactly-once billing-account creation and linking under concurrency.
  • A concurrent first-billing race can leave a transient orphaned Stripe customer (the lock-loser's); only the lock-winner's customer is linked.
  • [Issue #1805] Stamp the X-Request-ID response header inside the exception handler so error responses carry it independently of global-middleware ordering.
  • [Issue #1910] Make the workspace-safe .claude/pre-pr-gate.md gate diff-aware: it now runs only the lint and tests a change actually triggers (adding markdown, OpenAPI, version-script, and PHP-syntax lint coverage alongside the existing phpcs), runs PHP unit tests targeted to the changed files instead of the full suite, and delegates the full unit suite and integration tests to CI.

Fixed​

  • [Issue #1945] Skip the openapi-docs-drift CI backstop on in-repo release PRs (base master), not just develop PRs, so the release version the Auto Version Bump stamps into openapi.json no longer fails the drift check and blocks the release. Fork PRs and other base branches still run the check.
  • [Issue #1941] Reword the #1782 changelog entry so it no longer reproduces the literal merge-resolver sentinel strings, which tripped the conflict-marker guard once the fragment was collated into CHANGELOG.md and blocked PRs. CHANGELOG.md remains fully scanned by the guard.
  • [Issue #1939] Release the base stable version from a beta pre-release tag on master (e.g. 0.9.0.beta-N → 0.9.0) instead of bumping patch past it (0.9.1).
  • build/version.sh now strips a .beta- suffix to its base version on the master arm, mirroring the existing RC handling; hotfix/dependabot builds still take patch+1 once a stable tag exists.
  • [Issue #1467] Restrict GET /v1/stats/{type} to an allowlist of known model types, returning 404 for any other value to close a SQL-injection surface in the stats table lookup.
  • [Issue #1522] Convert generic exceptions thrown from services, jobs, helpers, and model hooks into typed ApiException subclasses so HTTP request paths return JSON:API-shaped errors instead of {"errors": []} 500s.
  • [Issue #1504] Remove the broken GET /v1/genres/{id}/deleted route that referenced a non-existent controller method and returned a 500.
  • Also drops the now-dead links.archive field — pointing at that removed route — from both the trashed-genre serialization (GET /v1/genres/deleted) and the DELETE /v1/genres/{id} archive response.
  • [Issue #1473] Escape DB credentials with escapeshellarg in DatasetRestoreCommand to close a shell-injection vector.
  • [Issue #1508] Populate Bruno v1 write-request bodies with schema-derived examples and fix the generators that emitted empty/stub bodies.
  • The example synthesizer now maps a JSON:API resource id from the sibling type (so top-level data.id and polymorphic oncard ids resolve to {{testCardId}}/{{testSetId}}/{{testPlayerId}}), aligns update-request URL path ids with the body data.id, and skips the JSON body for multipart/non-JSON endpoints.
  • Fixed the OpenAPI request schemas for years (use name, not year) and attributes (add the required attribute field) so the generated examples are runnable against the API.
  • [Issue #1512] Guard the ModelNotFoundException handler so unmapped models return a 404 instead of crashing with a 500.
  • [Issue #1513] Return a populated JSON:API error body for abort()-raised HttpExceptions instead of {"errors": []}.
  • [Issue #1514] Return JSON:API errors[] envelope from CheckWriteScope and CheckDeleteScope 403 responses instead of the non-conformant {error, message} shape.
  • [Issue #1526] Cache the api rate limiter's per-request OAuth client lookup behind a short-TTL key, removing two uncached DB queries from every authenticated request; stale entries expire via the TTL, with a forgetRateLimitCache() helper available for explicit invalidation.
  • [Issue #1530] Eager-load the polymorphic onCardable relation on Card::onCard() so OnCardTransformer's defaultIncludes no longer fires one lookup per on-card entity. A 25-card list with ?include=oncard drops from ~100 queries to a handful (#1530)
  • [Issue #1575] Fix queue-mode checklist import creating duplicate cards and dropping player/team data on large imports.
  • Adds a soft-delete-aware unique index on api_cards (set_id, number, section); the migration soft-deletes existing live duplicates first.
  • [Issue #1579] Honor force/confirm on DELETE /v1/sets/{id}: refuse deleting a published set or one with remaining children with a structured 409 naming the blocker, and cascade-delete children when force=true, instead of returning an opaque 500 (#1579).
  • [Issue #1587] Use the default GITHUB_TOKEN for the auto-version-bump PR checkout so a set-but-invalid DEPLOY_APP_TOKEN no longer shadows the fallback and fails the check.
  • [Issue #1600] Register the advertised read:draft OAuth scope so the token endpoint accepts it instead of rejecting it as invalid.
  • Caused by a duplicate Passport::tokensCan() registration that clobbered the scope map; the registrations are now consolidated into a single authoritative call.
  • [Issue #1621] Scope the minimatch npm override so it no longer caps markdownlint-cli at v5, fixing the lint:md named-export crash on newer Node.
  • [Issue #1651] Fix GET /v1/genres?name= 500 by adding a name query scope to the Genre model.
  • [Issue #1662] Advertise application/vnd.api+json for all JSON:API (/v1, /v2) request and response bodies in the OpenAPI spec, matching the media type the runtime enforces, by converting the source OA\JsonContent annotations to explicit OA\MediaType entries and regenerating public/docs/openapi.json.
  • The non-JSON:API /health and /version operations intentionally remain application/json.
  • [Issue #1690] Echo captured version-bump output before failing in the release workflows so a non-zero version-bump exit is diagnosable.
  • [Issue #1691] Replace the deprecated ::set-output workflow commands in build/create-host.sh with the $GITHUB_OUTPUT environment-file form.
  • [Issue #1692] Remove the deprecated install: true input from the Docker Buildx setup step in all four build workflows.
  • [Issue #1694] Fix the doc generator so single-resource example links.self URLs are canonical instead of the doubled /{id}/<uuid> form.
  • [Issue #1734] Resolve successor-URL placeholders in deprecation Link headers so per-resource deprecated aliases emit a concrete, dereferenceable URI instead of a literal RFC-3986-invalid template.
  • [Issue #1735] Update the committed OpenAPI spec for /v1/player-teams/{id} to the PATCH-only update convention and add a drift-guard test asserting every legacy v1 update path exposes patch and omits put.
  • [Issue #1753] Preserve caller-supplied abort(405, "...") messages in the JSON:API detail instead of dropping them for the canned Method Not Allowed error.
  • [Issue #1783] Stop the public doc generator from emitting auth headers and a bogus /v1 alias for unauthenticated infrastructure endpoints, and fix the deprecated /v2 alias to point at the correct /v1 path.
  • [Issue #1783] Stop the public doc generator from appending an example UUID to the links.self URL of singleton and action endpoints (e.g. /v1/me, /v1/sets/{id}/sync-parallel) that have no /{id} member route, so the documented self link is the canonical path rather than an invalid sub-resource URL.
  • [Issue #1794] Derive changelog compare-link predecessor from the last documented version instead of the latest git tag.
  • [Issue #1803] Honor ?include=oncard on the card list endpoint and batch-load onCard.onCardable, removing the per-card N+1 on the on-card relation.
  • [Issue #1806] Regenerate the committed OpenAPI spec and API reference docs from the PHP annotations — restoring dropped internal endpoints (e.g. sales-readiness), showing real JSON:API resource types in example responses instead of operation tags, and documenting previously-undocumented response schemas — and guard against future drift with a CI check plus automatic doc regeneration on develop PRs.
  • The docs generator now resolves $ref/array items.$ref to the component schema's resource type, and content types in the spec match what the controllers actually return.
  • [Issue #1856] Pin the in-memory array cache store for the test suite so the Set query-count / cache-N+1 guard tests measure domain queries only and no longer fail when the ambient cache store resolves to database (#1528).
  • The guards assert against DB::getQueryLog(); under the database cache store, cache() has/get/put and Cache::remember issued real cache-table queries that inflated the counts. The driver is now forced before app boot so boot-time singletons (e.g. the RateLimiter) capture the array store regardless of environment.
  • [Issue #1863] The ?name= filter on GET /v1/attributes now filters results instead of throwing a 500 — the name() query scope was undefined on the Attribute model. The scope is now shared via a FiltersByName trait used by Set, Genre, and Attribute (replacing the per-model copies), and ignores non-scalar input such as ?name[]=foo rather than erroring.

Security​

  • [Issue #1487] Set TrustProxies trusted-subnet list (env-overridable via TRUSTED_PROXIES) so the real client IP resolves behind the load balancer.
  • Operators behind a load balancer should set TRUSTED_PROXIES to their LB subnet; unset falls back to RFC-1918 private ranges.
  • [Issue #1490] Add an explicit $hidden array to the User model so password/remember_token are hidden from serialization by default (excluded unless explicitly made visible); the authenticated-user response is the narrow id/name/email JSON:API shape served by GET /v1/me, with the deprecated GET /user alias (#1502) pointing at the same controller.
  • [Issue #1488] Re-encode all image uploads (JPEG included) through Intervention before storage to strip embedded metadata and defeat polyglot-JPEG payloads.
  • [Issue #1474] Add fast-xml-parser, uuid, tmp, ws, and qs minimum-version package.json overrides (^ ranges, matching the existing overrides block) to clear the @usebruno/cli dev-chain npm audit advisories.
  • [Issue #1476] Add HSTS (on secure requests) and Permissions-Policy headers to the SecurityHeaders middleware and drop the deprecated X-XSS-Protection header.
  • [Issue #1477] Restrict the nginx /status-nginx and PHP-FPM /status endpoints to loopback and the private subnet, keeping /ping public for healthchecks.
  • [Issue #1479] Remove the behavioral bypass of the deprecated X-TCAPI-Ignore-Status header: the header no longer changes filtering, and unfiltered status access is governed by the read:all-status scope regardless of header presence.
  • Behavior change: clients holding only read:published (or no status scope) no longer see draft/archived content via the header.
  • [Issue #1481] Gate the OverrideDefaultStatus middleware behind a read:all-status scope check and a status allowlist before honoring the X-TCAPI-Default-Status header.
  • [Issue #1482] Reject per_page/limit over 100 on the Card, Player, and Team list endpoints to close a pagination DoS vector.
  • [Issue #1483] Require the internal OAuth scope on the audit-log and internal workflow routes.
  • Breaking for API clients: tokens without the internal scope now get 403 on GET/POST /v1/audit-logs, the canonical workflow routes GET /v1/actionable-sets, POST /v1/todo-initialization-jobs, and GET /v1/todo-initialization-jobs/{job_id}, and their deprecated /v1/workflow/* aliases (GET /v1/workflow/actionable-sets, POST /v1/workflow/bulk-initialize, GET /v1/workflow/bulk-initialize/{job_id}); re-issue affected tokens with the scope. (GET /v1/sets/{id}/workflow is unaffected. The internal scope itself already existed for the /internal/analytics/* routes — #1483 extends its enforcement to workflow and audit-log routes.)
  • [Issue #1521] Broaden Handler::isApiCall() to recognize the application/vnd.api+json Accept header and the non-versioned API path prefixes (v[0-9]+/, health, internal/, user) so API errors render as JSON:API instead of leaking framework stack traces.
  • [Issue #1552] Add a TrustProxies regression test asserting $request->ip() honors X-Forwarded-For only from a trusted proxy and never spoofs the client IP from an untrusted peer or a wildcard config value.
  • [Issue #1557] Add an internal OAuth scope and move the workflow, set-todo, and audit-log routes off /v1/* onto the gated /internal/* prefix.
  • Breaking for API consumers: the moved endpoints are no longer served under /v1/* and require the internal scope (403 without it); SDK/MCP/admin/tools callers must migrate (companion issues #214, #411, #1339, #784).
  • [Issue #1707] Restructure AuthenticateWithApiOrClientCredentials so the request pipeline is invoked exactly once, closing a latent double-execution hazard on the auth-fallback path.
  • [Issue #1773] Provision COMPOSER_TOKEN via a BuildKit secret mount so it never lands in an image layer, the image filesystem, or docker history.
  • All eight build entrypoints (4 Makefile targets + 4 workflows) now supply the token as a secret; COMPOSER_TOKEN must be present in the build environment for make up* and as the COMPOSER_TOKEN repo secret for CI.
  • [Issue #1793] Confirm the residual uuid advisory (GHSA-w5hq-g745-h8pq) is remediated by the existing overrides.uuid (>=11.1.1) pin and dismiss the stale Dependabot alert.
  • Dev-scope only; the pin and a clean lock resolving uuid to 11.1.1 are now guarded by a regression test so the advisory cannot silently return.

Removed​

  • [Issue #1629] Remove the unused OverrideDefaultStatus middleware and its status.override alias as dead code.
  • [Issue #1634] Remove the dormant api:vintage-import command, its sole-consumer vintage database connection, and the dead VINTAGE_* environment variables; rename the misnamed testVintageImport test.
  • Operator follow-up: delete the three PROD_VINTAGE_* GitHub repo secrets manually — they are not referenced in any tracked file.
  • [Issue #1646] Remove the deprecated PUT update alias on all v1 apiResource endpoints; PATCH is now the only accepted update verb.
  • Breaking change: PUT-based update callers now receive 405; coordinate a tradingcardapi-sdk-php major release and deprecation-window announcement before shipping.
  • [Issue #1732] Remove the deprecated v1 genres/deleted literal-segment alias route at the v2 cutover.
  • Breaking for any client still calling the literal segment; migrate to GET /v1/genres?filter[status]=deleted.

📦 [0.8.49] - June 21, 2026​

Removed​

  • [Issue #1930] Fully retire the vintage DB integration now that the vintage database is decommissioned: delete the api:vintage-import console command (app/Console/Commands/VintageImport.php), the vintage connection in config/database.php, the VINTAGE_* entries in .env.local, and the stale import command reference in docs/DEVELOPMENT-SETUP.md.

Fixed​

  • [Issue #1930] Remove the retired VINTAGE_DB_* secrets from the production deploy path. build/deploy.sh, .docker/prod.docker-compose.yaml, and .github/workflows/deployment.yaml still hard-required VINTAGE_DB_HOST/VINTAGE_DB_USERNAME/VINTAGE_DB_PASSWORD, so production deploys aborted with Missing required deploy secret: VINTAGE_DB_HOST once the corresponding PROD_VINTAGE_DB_* repository secrets were retired. This mirrors the removal already present on develop.

📦 [0.8.48] - June 21, 2026​

Changed​

  • npm(deps-dev): Bump undici from 7.27.2 to 7.28.0 (#1894)

📦 [0.8.47] - June 21, 2026​

Changed​

  • npm(deps): Bump markdown-it and markdownlint-cli (#1820)

📦 [0.8.46] - June 21, 2026​

Changed​

  • github-actions(deps): Bump shivammathur/setup-php (#1798)

📦 [0.8.45] - June 21, 2026​

Security​

  • [Issue #1926] Bump guzzlehttp/guzzle (7.11.1 → 7.12.1) and guzzlehttp/psr7 (2.11.0 → 2.12.1) to clear three medium Dependabot advisories: CVE-2026-55767 (dot-only cookie domains matching all hosts), CVE-2026-55568 (silent HTTPS-proxy downgrade to cleartext), and CVE-2026-55766 (CRLF injection in HTTP start-line serialization). Both packages are pulled in transitively (via Laravel, l5-swagger, and stripe-php); parent constraints already allowed the patched versions, so this is a composer.lock-only bump with no composer.json change. composer audit is now clean.

Fixed​

  • [Issue #1926] Load chalk via dynamic import() in the docs/scripts documentation generator and validator. A Dependabot bump to chalk 5 (ESM-only) broke the CommonJS require('chalk') calls, failing the "Auto Version Bump for Release PRs" workflow with ERR_REQUIRE_ESM. Switching to dynamic import keeps the v5 upgrade rather than pinning back to v4 (which Dependabot would re-bump).

📦 [0.8.44] - June 20, 2026​

Fixed​

  • Deploy script now owns the storage mount as the configured deploy identity (SSH_USERNAME) instead of a hardcoded user, supporting the migration off the legacy circleci deploy user (#1739)

📦 [0.8.43] - June 15, 2026​

Security​

  • [Issue #1788] Remediate open Dependabot alerts on master. Update vulnerable Composer dependencies pulled in transitively via Laravel (Symfony components to 7.4.12+, guzzlehttp/psr7 to 2.11.0, phpseclib/phpseclib to 3.0.55) and bump laravel/framework to 12.62.0 to close CVE-2026-48019 (CRLF injection in the default email rule). Update root npm dev tooling to clear the brace-expansion and fast-xml-parser advisories, and bump @usebruno/cli 3.2.2 → 3.4.2 (which supersedes Dependabot #1591). composer audit is now clean. The dev-only uuid advisory (GHSA-w5hq-g745-h8pq) is not resolved: uuid moves 9.0.1 → 10.0.0, but the fix requires >= 11.1.1 and @usebruno/js still pins uuid <11.1.1 upstream, so npm audit reports no available fix. Tracked in #1793.

Changed​

  • [Issue #1788] Add a Dependabot npm configuration entry for the /docs/scripts project so its dependencies are scanned for updates. The root / entry never covered this second npm project, leaving it a blind spot for version and security updates.

📦 [0.8.42] - June 14, 2026​

Changed​

  • npm(deps-dev): Bump protobufjs from 7.5.5 to 7.6.1 (#1595)

📦 [0.8.41] - June 14, 2026​

Changed​

  • npm(deps-dev): Bump axios from 1.15.1 to 1.17.0 (#1653)

📦 [0.8.40] - June 14, 2026​

Changed​

  • npm(deps-dev): Bump tmp from 0.2.5 to 0.2.7 (#1594)

📦 [0.8.39] - June 14, 2026​

Changed​

  • npm(deps-dev): Bump qs from 6.15.0 to 6.15.2 (#1588)

📦 [0.8.38] - June 14, 2026​

Changed​

  • chore(deps): Bump brace-expansion from 5.0.5 to 5.0.6 in /docs/scripts (#1585)

📦 [0.8.37] - June 14, 2026​

Changed​

  • npm(deps-dev): Bump @protobufjs/utf8 from 1.1.0 to 1.1.1 (#1580)

📦 [0.8.36] - June 14, 2026​

Changed​

  • npm(deps-dev): Bump fast-xml-builder from 1.1.4 to 1.2.0 (#1577)

📦 [0.8.35] - June 14, 2026​

Changed​

  • github-actions(deps): Bump the github-actions group across 1 directory with 6 updates (#1669)

📦 [0.8.34] - June 14, 2026​

Changed​

  • npm(deps-dev): Bump @grpc/grpc-js from 1.14.3 to 1.14.4 (#1733)

📦 [0.8.33] - June 13, 2026​

Security​

  • [Issue #1469] Scrub the committed OAuth clientId, clientSecret, and personal access token JWT from the Bruno local environment file (docs/bruno/environments/local.bru), switching them to {{process.env.LOCAL_*}} refs to match the staging/production envs. The personal access token var is also renamed from the dead ~personalAccessToken to the bare personalAccessToken that the Bruno scripts, README, and other envs actually read. Repo edit only — operators must still revoke the exposed client and scrub git history out of band (#1469)

📦 [0.8.32] - June 7, 2026​

Security​

  • [Issue #1468] Remove the APP_KEY from the tracked .env.local; the key now lives only in the gitignored .env, auto-generated by local provisioning on first startup. The Docker image's baked default .env no longer contains an encryption key, so containers fail closed (MissingAppKeyException) unless the runtime environment supplies APP_KEY (#1468)
  • [Issue #1468] Replace the shared committed APP_KEY in the CI integration-test and local full-stack compose files with distinct, documented throwaway keys; unit tests now use a dedicated throwaway key defined in phpunit.xml (#1468)

📦 [0.8.31] - June 7, 2026​

Security​

  • [Issue #1466] Pass the GitHub COMPOSER_TOKEN to Docker builds as a BuildKit secret mount instead of a build-arg, exposing it to composer only via COMPOSER_AUTH for a single RUN — the token can no longer appear in any image layer, auth.json, or docker history metadata. Applied across all four CI build workflows, the Makefile, and docker-compose builds (#1466)
  • Exclude .env and auth.json from the Docker build context via .dockerignore so local secrets can never enter an image layer through COPY . . (#1466)
  • The previously-leaked COMPOSER_TOKEN PAT has been rotated and revoked; the replacement is stored as a GitHub org-level secret (#1466)

📦 [0.8.30] - June 6, 2026​

Changed​

  • Slim the production Docker image by ~400MB: purge the PHP extension build toolchain ($PHPIZE_DEPS — gcc, g++, make, etc.) and -dev header packages in the same layer the extensions are compiled, keeping only the runtime shared libraries they link against (#1671)
  • Drop apt packages installed for PHP extensions that are never built: libicu-dev, libpq-dev, libmcrypt-dev, libedit-dev, libreadline-dev, libssl-dev (#1671)
  • Move build/dev tooling (git, openssh-client, gnupg, rsync, wget, jq, dos2unix) out of the production image into the test stage; gnupg is installed and purged within the nginx-install layer in base (#1672)
  • Extend the production-image CI guard to assert the compiler toolchain, git, gpg, ssh, wget, and rsync are absent and that the gd, pdo_mysql, zip, and pcov extensions still load (#1671, #1672)

📦 [0.8.29] - June 6, 2026​

Changed​

  • Restructure Dockerfile into multi-stage build (base → test → prune → production). The published production image no longer contains test suites (tests/), documentation (docs/), build tooling (build/), dev composer packages (phpunit, faker, mockery, phpcs), npm node_modules, the Node.js runtime, or the Bruno CLI — reducing image size by several hundred MB. Local dev and CI test jobs use the test target, which preserves previous behavior (#1667)
  • Add a CI guard step to build-and-test.yaml that builds the production target on every PR and verifies it contains no test/dev artifacts while keeping required runtime files (#1667)

Security​

  • Stop baking the GitHub COMPOSER_TOKEN into images: composer config github-oauth previously wrote /var/www/app/auth.json, which shipped in every published image. The token file is now removed immediately after dependency installation and excluded from the production stage (#1667)

📦 [0.8.28] - June 5, 2026​

Security​

  • Remove hardcoded production secrets (APP_KEY, database credentials) from .docker/prod.docker-compose.yaml; values are now supplied from GitHub Actions secrets at deploy time and rendered into a chmod 600 .env file on the host by deploy.sh (#1464)

📦 [0.8.27] - April 19, 2026​

Changed​

  • npm(deps-dev): Bump @stoplight/spectral-cli from 6.15.0 to 6.15.1 (#1450)

📦 [0.8.26] - April 19, 2026​

Changed​

  • github-actions(deps): Bump actions/cache in the github-actions group (#1451)

📦 [0.8.25] - April 19, 2026​

Changed​

  • Skip the per-PR auto-version-bump workflow for Dependabot PRs to prevent cross-PR conflicts on CHANGELOG.md and public/docs/openapi.json when other PRs merge to master. Version-bump still runs post-merge via build-release.yaml / build-develop.yaml so the version history is unaffected (#1452)

📦 [0.8.24] - April 19, 2026​

Security​

  • Bump laravel/passport to ^13.7.1 addressing TokenGuard authenticating unrelated user for client-credential tokens (GHSA #138, high) (#1448)
  • Bump phpunit/phpunit to ^12.5.22 addressing argument injection via newline in INI values (GHSA #143, high) (#1448)
  • Bump transitive protobufjs to 7.5.5 via npm override addressing arbitrary code execution (GHSA #141, critical) (#1448)
  • Bump transitive axios to 1.15.0+ via npm override addressing NO_PROXY SSRF and cloud-metadata exfiltration (GHSA #140, #142) (#1448)
  • Bump transitive follow-redirects to 1.16.0 via npm override addressing cross-domain auth header leak (GHSA #139) (#1448)
  • Bump transitive phpseclib/phpseclib to 3.0.51 addressing variable-time HMAC comparison in SSH2 (GHSA #135, low) (#1448)

Changed​

  • Bumped GitHub Actions (SHA-pinned): appleboy/ssh-action 1.2.2→1.2.5, chrnorm/deployment-action 2.0.5→2.0.8, actions/github-script v8→v9 (#1448)
  • Grouped github-actions dependabot updates into a single PR to reduce merge churn and keep SHA pinning consistent (#1448)
  • Bumped dotenv dev dependency to ^17.4.2 (#1448)

📦 [0.8.23] - April 19, 2026​

Added​

  • todo_id and notes fields on GET /v1/workflow/actionable-sets response items so the admin review-queue UI can render reasons and wire Resolve/Skip actions without a second lookup (#1445)
  • notes field on workflow todo responses (GET /v1/sets/{id}/workflow and GET /v1/workflow/sets/{id}/todos) (#1445)

📦 [0.8.22] - April 16, 2026​

Added​

  • Added analyze:sales-readiness artisan command for data coverage and completeness reporting (#1431)
  • Overview of published vs draft sets and cards
  • Genre breakdown with on-card relationship coverage rates
  • Set metadata fill rates (manufacturer, brand, year, genre)
  • Year coverage breakdown by genre with expected vs actual card counts and --years=N flag
  • Most recent year with published card data callout in overview
  • Image coverage baseline (0% as of initial report)
  • Workflow status and publish-readiness tracking
  • Supports --format=table (default) and --format=json output

📦 [0.8.21] - April 13, 2026​

Security​

  • Pin all GitHub Actions in workflow files to full commit SHAs for supply chain security hardening (#1427)

📦 [0.8.20] - April 11, 2026​

Added​

  • Added audit logging system with polymorphic api_audit_logs table and REST endpoints (GET /v1/audit-logs, POST /v1/audit-logs) for centralized orchestration event tracking (#1376)

📦 [0.8.19] - April 11, 2026​

Added​

  • Added manual audit enrichment step to set workflow that auto-creates when publish completes (#1375)
  • Added step=audit filter to GET /v1/workflow/actionable-sets endpoint for surfacing audit-pending sets
  • Added migration to backfill audit todos for all previously published sets

📦 [0.8.18] - April 11, 2026​

Added​

  • Added auto-assign sprint workflow to automatically assign new issues to the current sprint iteration (#1418)

📦 [0.8.17] - April 9, 2026​

Added​

  • Added review status to set_todos for flagging steps that need human intervention (#1395)
  • Added status query parameter to GET /v1/workflow/actionable-sets endpoint to filter by todo status (e.g. status=review)

📦 [0.8.16] - April 9, 2026​

Fixed​

  • Fixed deployment status stuck in in_progress when docker-tag-latest job fails by adding a dedicated status-finalizer job (#1407)

📦 [0.8.15] - April 4, 2026​

Removed​

  • Removed unused markdown-toc dependency and 59 transitive packages (including deprecated lodash.template) from docs/scripts (#1405)

📦 [0.8.14] - April 4, 2026​

Fixed​

  • Pin third-party GitHub Actions to specific versions to resolve deprecated set-output warnings (#1404)

📦 [0.8.13] - April 4, 2026​

Changed​

  • Replace deprecated avakar/create-deployment@v1 and avakar/set-deployment-status@v1 with chrnorm/deployment-action@v2 and chrnorm/deployment-status@v2 for Node.js 24 compatibility (#1403)

📦 [0.8.12] - April 4, 2026​

Security​

  • Resolved 27 security vulnerabilities across npm and Composer dependencies (2 critical, 9 high, 10 medium, 1 low)
  • Updated phpseclib/phpseclib (3.0.49 → 3.0.50) to fix AES-CBC padding oracle timing attack
  • Updated league/commonmark (2.8.1 → 2.8.2) to fix embed extension allowed_domains bypass
  • Added npm overrides for lodash, minimatch, and rollup to resolve transitive vulnerabilities in @stoplight/spectral-cli

Changed​

  • Updated all npm dependencies to latest compatible versions
  • Updated Composer dependencies (phpseclib/phpseclib, league/commonmark)

📦 [0.8.11] - April 4, 2026​

Fixed​

  • Actionable-sets endpoint no longer times out for priority=backlog and priority=all on validate step (#1374)

📦 [0.8.10] - April 4, 2026​

Added​

  • workflow:bulk-validate command to deterministically validate and publish legacy draft sets (#1390)

📦 [0.8.9] - April 3, 2026​

Fixed​

  • Parallel sets no longer appear in actionable-sets queue until parent is published (#1389)

📦 [0.8.8] - March 29, 2026​

Fixed​

  • cleanup:subset-source-satisfaction command now excludes non-parallel subsets inheriting parent-source satisfaction when their parent set is soft-deleted; parallel sets remain eligible for unconditional fixing (#1391)

📦 [0.8.7] - March 29, 2026​

Fixed​

  • bulk-initialize endpoint now returns diagnostic metadata (resolved_count, meta) so callers can understand why 0 sets were initialized (#1371)
  • bulk-initialize parent-source lookup now excludes soft-deleted parent sets, consistent with SetObserver behavior (#1384)

📦 [0.8.6] - March 29, 2026​

Fixed​

  • bulk-initialize endpoint now returns diagnostic metadata (resolved_count, meta) so callers can understand why 0 sets were initialized (#1371)

📦 [0.8.5] - March 29, 2026​

Fixed​

  • Parallel sets (is_parallel) now unconditionally skip discover_sources step, regardless of parent source state (#1387)
  • cleanup:subset-source-satisfaction command now also fixes parallel sets with pending discover_sources (#1387)

📦 [0.8.4] - March 27, 2026​

Fixed​

  • Subsets (sets with parent_id) now inherit source satisfaction from parent set, not just parallel sets. Non-parallel subsets no longer appear in the "needs sources" workflow queue when their parent already has sources (#1382)

Added​

  • cleanup:subset-source-satisfaction artisan command to retroactively fix existing non-parallel subset workflows (#1382)

📦 [0.8.3] - March 22, 2026​

Added​

  • GET /v1/workflow/actionable-sets: comprehensive response field contract tests validating

Performance​

  • GET /v1/workflow/actionable-sets: add composite index api_cards(set_id, deleted_at) to

📦 [0.8.2] - March 22, 2026​

Fixed​

  • GET /v1/workflow/actionable-sets?step=validate 504 gateway timeout — added composite indexes on api_set_todos(set_workflow_id, sort_order, status) and (step, status) to eliminate full-table scans in the prior-step anti-join, plus a 5-second application-level query timeout guard that returns 503 instead of hanging (#1369)

📦 [0.8.1] - March 22, 2026​

Added​

  • GET /v1/workflow/sets/{id}/todos endpoint returning the ordered todo list for a set's workflow (#1367)

📦 [0.8.0] - March 21, 2026​

Added​

  • set_workflows, set_todos, and set_todo_history tables for API-side workflow state tracking (#1310)
  • WorkflowStep and TodoStatus PHP enums
  • SetWorkflow, SetTodo, and SetTodoHistory Eloquent models with relationships
  • creating observers on SetTodo and SetTodoHistory to auto-derive set_id from parent record, preventing denormalized field mismatches
  • set_id index added to api_set_todo_history migration
  • PATCH /v1/set-todos/{id} endpoint to update workflow todo status (#1311)
  • Step dependency enforcement: returns 422 DEPENDENCY_NOT_MET when prior steps are not completed or skipped
  • Validate/cleanup loop: completing validate with issues resets cleanup to pending; completing cleanup resets validate to pending
  • Timestamps are cleared when a todo transitions to pending, skipped, or blocked
  • Auto-initialize set_workflow and set_todos on set creation based on set type (#1312)
  • Workflow todos are also initialized for published sets: all steps skipped except discover_sources, which stays pending when no source URL has been recorded
  • Parallel sets inherit discover_sources satisfaction from their parent
  • DiscoverSources step added to workflow for parallel, insert, autograph, and relic sets (#1348)
  • GET /v1/workflow/actionable-sets endpoint — returns sets where a given step is pending and all prior steps are completed or skipped (#1313)
  • Includes updated_at (ISO 8601) field in response items (#1344)
  • POST /v1/workflow/bulk-initialize endpoint — initializes workflow todos for pre-existing sets without workflows (#1315)
  • Returns 202 Accepted when dispatched asynchronously (> 100 sets)
  • GET /v1/workflow/bulk-initialize/{job_id} status endpoint — polls async bulk-initialize job status (#1323)
  • GET /v1/sets/{id}/workflow endpoint — exposes full set workflow state and todos (#1340)
  • Enrichment step architecture (#1349)
  • isEnrichment() method on WorkflowStep enum classifies orthogonal steps that do not depend on prior workflow steps
  • GET /v1/workflow/actionable-sets and PATCH /v1/set-todos/{id} bypass prior-step ordering for enrichment steps
  • Response example rendering in internal API reference template, matching public template behavior (#1333)
  • Unit tests for isJsonApiSchema, generateExampleValue, and generateExampleFromSchema helper functions in generate-repo-docs.js (#1334)

Changed​

  • Dependency updates: npm packages, Composer packages, and GitHub Actions (#1328)
  • Updated laravel/framework, laravel/passport, spatie/laravel-activitylog, and other Composer dependencies
  • Updated darkaonline/l5-swagger from v9 to v10, firebase/php-jwt from v6 to v7, zircote/swagger-php from v5 to v6
  • Updated @usebruno/cli from v2 to v3
  • Upgraded glob from v8 to v11 in docs/scripts (#1330)

Fixed​

  • All workflow endpoint success responses now include Content-Type: application/vnd.api+json header (#1342)
  • Doc generator now renders accurate example responses for flat JSON endpoints instead of a generic JSON:API placeholder (#1327)
  • CHANGELOG compare link ordering: [Unreleased] now always points to the highest version, and link definitions are sorted in descending version order (#1337)
  • Resolved 39 PHPUnit notices in test suite from FakerPHP deprecated property access and PHPUnit 12 mock object expectation changes (#1324)
  • Eliminated redundant DB query in SetObserver for parallel set creation by reusing a single parent lookup for both card count and source count (#1351)
  • SetObserver::created() now treats a trashed parent as absent to avoid dispatching no-op workflow initialization jobs (#1354)

📦 [0.7.68] - February 23, 2026​

Added​

  • set_id attribute in set source API responses for all endpoints (GET /v1/set-sources, GET /v1/set-sources/{id}, POST, PUT)

📦 [0.7.67] - February 23, 2026​

Added​

  • filter[set_id] query parameter support on GET /v1/cards endpoint for filtering cards by set

📦 [0.7.66] - February 23, 2026​

Added​

  • OpenAPI annotation for DELETE /v1/sets/{id}/checklist endpoint
  • Input validation for from_number and to_number query parameters on delete checklist endpoint

📦 [0.7.65] - February 23, 2026​

Changed​

  • Tuned PHP-FPM pool settings to prevent max_children saturation under load
  • PHP-FPM config now uses environment variables for per-environment tuning

📦 [0.7.64] - February 22, 2026​

Fixed​

  • Double logging of HTTP requests — disabled redundant PHP-FPM access log that showed rewritten /index.php URLs (#1289)

📦 [0.7.63] - February 22, 2026​

Added​

  • DELETE /v1/sets/{id}/checklist endpoint for bulk card deletion with ?queue=true async support

📦 [0.7.62] - February 22, 2026​

Fixed​

  • Card delete operations timing out due to synchronous cache invalidation and unoptimized cascade deletes (#1287)
  • Orphaned OnCard and ObjectAttribute records left behind when parallel cards are cascade-deleted (#1287)

Changed​

  • Deferred cascade cache invalidation in CardObserver to run after response is sent, improving API response times
  • Replaced per-child deleteQuietly() loop with batch queries for cascade deletes (3 queries instead of N)
  • Added composite index on api_object_attributes (attributable_id, attributable_type) for faster lookups

📦 [0.7.61] - February 19, 2026​

Security​

  • Updated firebase/php-jwt from v6.x to v7.x resolving weak encryption advisory
  • Updated transitive npm dependencies resolving fast-xml-parser, qs, and markdown-it advisories

Changed​

  • Updated darkaonline/l5-swagger from v9 to v10 (drops Doctrine annotations, project uses PHP 8 attributes)
  • Updated @usebruno/cli from v2 to v3 (Safe Mode default, added --sandbox=developer to all integration test commands)
  • Updated laravel/framework from 12.44.0 to 12.52.0
  • Updated laravel/passport from 13.4.2 to 13.4.4
  • Updated spatie/laravel-activitylog from 4.10.2 to 4.11.0
  • Updated laravel/tinker from 2.10.2 to 2.11.1
  • Updated zircote/swagger-php from v5 to v6
  • Updated phpunit/phpunit from 12.4.4 to 12.5.14

📦 [0.7.60] - February 19, 2026​

Fixed​

  • Rate limiter no longer grants unlimited access to unverified JWT tokens (#1282)
  • Clients with rate_limit = null or 0 now get the default authenticated limit instead of Limit::none()
  • Unknown/unmatched tokens are keyed by IP to prevent bucket-splitting attacks with rotated fake jti values

📦 [0.7.59] - February 17, 2026​

Fixed​

  • Per-client rate limiting now works with JWT tokens (#1280)
  • Was silently falling back to default limits because the full JWT string was used for DB lookup instead of extracting the jti (token ID) claim
  • Opaque tokens (used in tests) continue to work unchanged

📦 [0.7.58] - February 16, 2026​

Fixed​

  • Set name search (name=like:...) now returns matching child sets (inserts/relics/parallels) when no parent_id is specified (#1278)

📦 [0.7.57] - January 25, 2026​

Fixed​

  • fix: Use dev nginx config with self-signed certs in CI mode

📦 [0.7.56] - January 25, 2026​

Added​

  • feat: Add DISABLE_WORKERS env var to skip queue workers in CI

📦 [0.7.55] - January 21, 2026​

Changed​

  • Set sources now allow multiple sources of the same type per set (#1256)

📦 [0.7.54] - January 18, 2026​

Fixed​

  • OnCard::sync now properly handles duplicate playerteams - deduplicates input and can delete existing duplicates (#1249)

Removed​

  • Temporary debug logging from OnCard and CardHelpers (#1247, #1248)

📦 [0.7.53] - January 18, 2026​

Fixed​

  • fix: Use error_log() for Docker-visible debug output (#1246)

📦 [0.7.52] - January 18, 2026​

Added​

  • debug: Add logging to trace OnCard sync deletion (#1246)

📦 [0.7.51] - January 10, 2026​

Fixed​

  • Parallel sets now inherit current_card_count from parent when card_count is null (#1235)

📦 [0.7.50] - January 10, 2026​

Changed​

  • Added token efficiency guidelines to CLAUDE.md (targeted testing, agent best practices, recommended permissions) (#1234)
  • Removed local /issue command - use cardtechie-assistant MCP skill instead (#1234)

📦 [0.7.49] - January 9, 2026​

Added​

  • Set sources endpoint now supports set_id and source_type query parameter filtering (#1232)

Fixed​

  • Set sources can no longer be created for soft-deleted sets (#1232)
  • Recreating a set source after deletion no longer returns 409 Conflict (#1232)

Changed​

  • Set sources now use hard delete instead of soft delete for cleaner data management (#1232)

📦 [0.7.48] - January 9, 2026​

Fixed​

  • Parallel set cards now inherit serial from the set instead of copying from parent card (#1227)

📦 [0.7.47] - January 6, 2026​

Fixed​

  • Child set titles no longer include parent set's serial number (#1228)

📦 [0.7.46] - January 2, 2026​

Fixed​

  • fix: Correctly parse filter[status] query parameter

📦 [0.7.45] - January 2, 2026​

Added​

  • feat: Add filter[status] query parameter to sets endpoint (#1220)

📦 [0.7.44] - December 30, 2025​

Changed​

  • Updated laravel/framework from 12.40.2 to 12.44.0
  • Updated laravel/passport from 13.4.1 to 13.4.2
  • Updated @usebruno/cli from 2.14.3 to 2.15.1
  • Updated markdownlint-cli from 0.46.0 to 0.47.0
  • Updated actions/upload-artifact from v5 to v6 in GitHub workflows
  • Updated actions/download-artifact from v6 to v7 in GitHub workflows
  • Updated actions/cache from v4 to v5 in GitHub workflows

Security​

  • Resolved high-severity vulnerability in jws (HMAC signature verification)
  • Resolved high-severity vulnerability in lodash.set (Prototype Pollution)
  • Resolved high-severity vulnerability in lodash.template (Command Injection)

📦 [0.7.43] - December 30, 2025​

Fixed​

  • PlayerTeam url() method now correctly includes v1 route prefix (#1216)

📦 [0.7.42] - December 29, 2025​

Added​

  • Set names now include serial number suffix for numbered parallels (#1213)
  • Uses standard hobby convention: /X for numbered sets (e.g., "Black Foil /10")
  • Shows 1/1 for superfractors (serial = 1)
  • Skips append if name already ends with a serial suffix to prevent duplicates
  • Run php artisan api:update-names --sets after deployment to update existing sets

📦 [0.7.41] - December 26, 2025​

Fixed​

  • Card deletion now removes ALL oncard relationships instead of only the first one (#1202)
  • Previously, deleting a card with multiple oncard entries (players, teams, playerteams) only removed the first entry
  • This caused orphaned OnCard records to remain in the database
  • Fixed by using bulk delete instead of firstOrFail()->delete()
  • Same fix applied to card attributes deletion

Added​

  • MCP tool get_card_oncard to list all oncard relationships in a simple, structured format (#1202)
  • Returns players, teams, and playerteams grouped by type with names
  • Designed for data correction workflows where you need to see what's attached to a card
  • MCP tool clear_card_oncard to remove all oncard relationships from a card (#1202)
  • Clears all players, teams, and playerteams at once to start fresh
  • Follows two-step confirmation pattern for safety

📦 [0.7.40] - December 26, 2025​

Fixed​

  • Parallel set sync now preserves on-card relationship order, ensuring multi-player card names match between parent and parallel sets (#1208)
  • Added sort_order column to api_on_card table with migration to backfill existing records
  • Updated OnCard::sync() to preserve array order as sort_order
  • Updated Card::onCard() relationship to order by sort_order
  • Updated CreateCardForParallelSet and UpdateCardForParallelSet jobs to preserve sort_order
  • Added sort_order to API response via OnCardTransformer

📦 [0.7.39] - December 26, 2025​

Added​

  • API endpoint test coverage for parallel set card_count inheritance (#1205)
  • Verifies card_count is inherited from parent when creating parallel sets via API
  • Confirms explicit card_count values are preserved when provided
  • Tests taxonomy inheritance (genre, manufacturer, year, brand)
  • Covers insert set behavior (should NOT inherit card_count)
  • Edge case tests for boundary conditions:
  • Inheritance of zero card_count from parent
  • Explicit zero card_count preservation
  • Parallel set without parent_id (no inheritance)
  • Taxonomy inheritance with explicit card_count
  • Very large card_count values (9999+)
  • Sets with both is_parallel and is_insert flags
  • Inheritance of null card_count from parent

📦 [0.7.38] - December 26, 2025​

Fixed​

  • CardController::update() now returns 404 instead of 500 when card doesn't exist (#1180)

📦 [0.7.37] - December 26, 2025​

Fixed​

  • Fix validation rule typo in AttributeRequest causing 500 error (#1203)

📦 [0.7.36] - December 25, 2025​

Added​

  • New POST /v1/sets/{id}/sync-parallel endpoint for syncing parallel sets (#1171)
  • Adds missing cards from parent set to parallel set
  • Copies on-card relationships (players, teams, playerteams) from parent cards
  • Copies card attributes with pivot values from parent cards
  • Returns count of created/skipped/updated cards
  • Supports async processing via ?queue=true parameter
  • Addresses limitation where ParallelizeSet only runs on initial parallel set creation

📦 [0.7.35] - December 25, 2025​

Added​

  • Explicit tests for current_card_count field accuracy after card changes (#1157)
  • Verifies cache invalidation correctly updates current_card_count in checklist responses
  • Tests card creation, deletion, and restoration scenarios for both V1 and V2 endpoints
  • Provides regression protection for the cache invalidation added in #1156

Changed​

  • Optimized cache invalidation performance (#1157)
  • Pre-computed include combinations as constant to eliminate runtime bit manipulation
  • Removed duplicate cache clearing logic from tests (now delegates to SetCacheHelper)
  • Reduced CPU overhead during cache invalidation operations

📦 [0.7.34] - December 24, 2025​

Fixed​

  • Checklist import now correctly parses serial numbers from end of line (#1154)
  • Input like Player - Team /10 now extracts serial as 10 instead of including /10 in team name
  • Supports both /10 and #/10 formats at end of line
  • Maintains backward compatibility with existing #/ 10 space-separated format

📦 [0.7.33] - December 23, 2025​

Fixed​

  • parent_id filter now works correctly in GET /v1/sets endpoint (#1146)
  • Previously the parameter was ignored and returned unrelated parent sets
  • Now correctly returns child sets (parallels/inserts) of the specified parent
  • Returns empty array when parent has no child sets

📦 [0.7.32] - December 23, 2025​

Added​

  • Year filtering capability to /v1/sets endpoint via year query parameter (#1191)
  • Filter sets by year ID (UUID) for targeting specific release years
  • Enables /prepare-publication workflow to focus on recent releases

📦 [0.7.31] - December 23, 2025​

Added​

  • subset_count field to Set API responses (#1192)
  • Returns the count of child subsets for each set
  • Enables UI to show expand indicators for sets with subsets
  • Uses eager loading via withCount to prevent N+1 queries

Fixed​

  • Optimized current_card_count field in SetTransformer to use eager loading (#1192)
  • Also uses withCount('cards') to prevent N+1 queries
  • Both subset_count and current_card_count now use efficient eager loading

📦 [0.7.30] - December 22, 2025​

Fixed​

  • SetSource API endpoint now properly accepts set_id during creation (#1188)

📦 [0.7.29] - December 22, 2025​

Added​

  • Queue parameter for POST /v1/sets/{id}/checklist endpoint (#1186)
  • queue=true dispatches import to background job for async processing
  • Prevents timeout for large checklists (50+ cards)
  • Default behavior unchanged (synchronous processing)

📦 [0.7.28] - December 21, 2025​

Added​

  • Override mechanism for parallel cards (#1163)
  • Fields can be explicitly marked as overridden to prevent sync from parent card
  • New field_overrides JSON column on cards tracks which fields should not be inherited
  • Valid overridable fields: title, number, description, is_rookie, serial, section
  • API support for viewing and managing field overrides on parallel cards
  • Auto-override: When a parallel card's propagatable field is directly updated, it's automatically marked as overridden
  • Two-layer approach: Explicit overrides take precedence over implicit detection

📦 [0.7.27] - December 21, 2025​

Added​

  • Cascade delete/restore for parallel cards (#1162)
  • When a base card is deleted, all parallel card children are automatically soft-deleted
  • When a base card is restored, all trashed parallel card children are automatically restored
  • Cache invalidation for affected parallel sets

📦 [0.7.26] - December 21, 2025​

Fixed​

  • CreateCardForParallelSet job now copies card attributes when creating parallel cards (#1178)

📦 [0.7.25] - December 20, 2025​

Added​

  • Update propagation for parallel cards (#1161)
  • When a base card is updated, changes automatically propagate to all parallel card children
  • Override detection: Fields that differ from the parent's original value are preserved
  • Propagates: title, number, description, is_rookie, serial, section
  • Also syncs onCard relationships and attributes
  • Uses queued jobs for performance (non-blocking API response)

📦 [0.7.24] - December 20, 2025​

Fixed​

  • SetController::update now properly triggers Eloquent observers (#1175)
  • Fixes is_parallel toggle not dispatching ParallelizeSet job via API
  • Enables MCP parallelize_set tool to work correctly

📦 [0.7.23] - December 20, 2025​

Added​

  • Queue workers now restart automatically after code updates in local development (#1173)
  • Prevents stale code issues when queue job logic changes

📦 [0.7.22] - December 20, 2025​

Fixed​

  • Team name parsing now correctly handles multi-word locations (St. Louis, New York, Los Angeles, etc.) (#1168)
  • Previously "St. Louis Cardinals" was incorrectly split as location="St.", mascot="Louis Cardinals"
  • Now correctly parses as location="St. Louis", mascot="Cardinals"
  • Supports 45+ known multi-word city/region names including Salt Lake City, Tampa Bay, Kansas City

Added​

  • New cleanup:fix-team-location-splits command to correct existing incorrectly-split team names
  • --analyze: Identify teams with potential incorrect splits
  • --dry-run: Preview proposed changes without applying
  • --genre=: Filter by specific sport/genre

📦 [0.7.21] - December 19, 2025​

Added​

  • Parallel set card creation now copies onCard relationships (players, teams, playerteams) from parent cards (#1169)
  • Cards in parallel sets now display full names with player info instead of just numbers
  • Card name auto-generation works correctly for parallel cards

📦 [0.7.20] - December 19, 2025​

Added​

  • Comma-delimited player/team format support in checklist import (#1166)
  • Converts Player Name, Team Name to Player Name - Team Name format automatically
  • Handles edge cases like name suffixes (Jr., Sr., II, III, etc.) correctly
  • Both comma and dash formats now work for checklist import

📦 [0.7.19] - December 19, 2025​

Added​

  • Ability to mark existing sets as parallel and have cards copied from parent (#1135)
  • SetObserver now dispatches ParallelizeSet job when is_parallel changes to true
  • Prevents duplicate card copying by checking if parallel set already has cards
  • Comprehensive test coverage for parallel card inheritance feature (#1135)
  • SetObserverTest: Tests for taxonomy inheritance, card_count inheritance, job dispatching
  • CardObserverParallelTest: Tests for parallel subset card creation
  • ParallelSetIntegrationTest: End-to-end workflow tests

Fixed​

  • CardObserver now correctly iterates all subsets when creating parallel cards (#1135)
  • Changed break to continue to process all parallel subsets instead of stopping at first non-parallel

Changed​

  • Improved robustness of parallel card job handlers (#1135)
  • ParallelizeSet job now validates set and parent exist before processing
  • CreateCardForParallelSet job validates set and card exist before replication
  • Added idempotency check to prevent duplicate card creation on job retries
  • Performance optimizations to reduce memory usage in observer queries
  • Added comprehensive test coverage for edge cases and race conditions

📦 [0.7.18] - December 17, 2025​

Added​

  • is_variation boolean column to sets table for variation sets (#1145)
  • Supports sets that share card numbering with parent but have different visual treatment (e.g., "Tin Type Variations", "Chrome Variations")
  • Added to Set model, SetTransformer, and OpenAPI schema

📦 [0.7.17] - December 17, 2025​

Added​

  • Add Laravel scheduler supervisor configuration for Docker containers (#1158)
  • Enables scheduled commands like stats:capture-snapshots to run automatically

📦 [0.7.16] - December 16, 2025​

Fixed​

  • Card deletion now invalidates set checklist cache to prevent stale data (#1155)
  • Added cache invalidation for V1 and V2 checklist endpoints
  • Cache is cleared on card create, update (number/name), delete, and restore
  • Supports both 'full' and 'compact' format cache keys

📦 [0.7.15] - December 15, 2025​

Fixed​

  • Fix "Class Oncard not found" error when assigning multiple players to a card (#1152)

📦 [0.7.14] - December 12, 2025​

Fixed​

  • Missing cache_locks table preventing scheduled jobs from running (#1142)

📦 [0.7.13] - December 2, 2025​

Added​

  • docs: Add OpenAPI documentation to stats snapshot endpoints

Fixed​

  • fix: Update stats endpoints to return SDK-compatible response format

📦 [0.7.12] - November 30, 2025​

Added​

  • feat: Add daily snapshot scheduled job (#1127)
  • stats:capture-snapshots Artisan command for capturing entity count snapshots
  • Scheduled to run daily at 04:00
  • --date option for backfilling historical data

📦 [0.7.11] - November 29, 2025​

Added​

  • feat: Add stats API endpoints (#1126)
  • GET /v1/stats/counts - Current entity counts for sets, cards, players, and teams
  • GET /v1/stats/snapshots - Historical snapshots with filtering by entity type and date range
  • GET /v1/stats/growth - Growth metrics by period (7d, 30d, 90d, 1y)

📦 [0.7.10] - November 29, 2025​

Added​

  • feat: Add entity snapshots table and model (#1125)

📦 [0.7.9] - November 29, 2025​

Fixed​

  • OAuth refresh token ID column too small causing login failures (#1123)

📦 [0.7.8] - November 27, 2025​

Fixed​

  • Fix 500 error when including oncard with orphaned records

📦 [0.7.7] - November 27, 2025​

Added​

  • Implement PlayerTeamController.show() method (#1110)
  • GET /v1/playerteams/{id} endpoint now functional with ?include=player,team support
  • Enables MCP tool get_playerteam to fetch individual playerteam records

Changed​

  • Update Docker base image to PHP 8.5-fpm (#1102)
  • Remove opcache from Dockerfile ext-install (built-in to PHP 8.5)
  • Disable xdebug in Dockerfile (not yet compatible with PHP 8.5)
  • Update all PHP dependencies (#1111):
  • laravel/framework 12.38.1 → 12.40.2
  • laravel/helpers 1.8.1 → 1.8.2
  • laravel/passport 13.4.0 → 13.4.1
  • laravel/tinker 2.10.1 → 2.10.2
  • nunomaduro/collision 8.8.2 → 8.8.3
  • phpunit/phpunit 12.4.2 → 12.4.4
  • slevomat/coding-standard 8.24.0 → 8.25.1
  • Update GitHub Actions: actions/checkout v5 → v6, PHP 8.3 → 8.5 in all workflows
  • Update JavaScript dependencies in docs/scripts

Fixed​

  • CardController.update() now respects the include query parameter (#1109)
  • PATCH responses now include relationship data when requested (e.g., ?include=oncard)
  • Enables MCP tools to verify relationship changes after card updates
  • Fix JsonApiHeader middleware for Symfony 8.x AcceptHeader behavior change

📦 [0.7.6] - November 24, 2025​

Added​

  • Bulk image upload endpoint (POST /v1/card-images/bulk) supporting up to 50 images per request (#1050)
  • Batch status tracking endpoint (GET /v1/card-images/bulk/{batch_id}) for polling upload progress
  • Laravel job batching for async image processing with progress tracking
  • Atomic mode option for all-or-nothing batch uploads

📦 [0.7.5] - November 21, 2025​

Changed​

  • Define OAuth scopes in AuthServiceProvider (Issue #889)

📦 [0.7.4] - November 16, 2025​

Changed​

  • Updated OpenAPI specification to document missing relationships (#1089)
  • Added images relationship to Card schema
  • Completes documentation for Card Images feature (v0.7.0)
  • Defined as one-to-many array of card_images resources
  • Added sources relationship to Set schema
  • Completes documentation for Set Sources feature (v0.6.0)
  • Defined as one-to-many array of set_sources resources

📦 [0.7.3] - November 14, 2025​

Changed​

  • github-actions(deps): bump actions/github-script from 7 to 8

📦 [0.7.2] - November 14, 2025​

Fixed​

  • Fix CHANGELOG.md version numbers after re-tagging 0.7.0 and 0.7.1 releases (#1085)

📦 [0.7.1] - November 14, 2025​

Fixed​

  • Fix version.sh to recognize 'Release TCAPI X.Y.Z' commit message pattern in addition to 'Release version X.Y.Z' (#1083)

📦 [0.7.0] - November 14, 2025​

Added​

  • Card Images System - Complete card image management system (#1045, #1046, #1047, #1048)
  • Full CRUD API at /v1/card-images with multipart file upload support
  • Support for front and back card images with unique constraint per card
  • File validation: JPEG, PNG, WebP (up to 10MB, max 4000x4000px)
  • Automatic JPEG standardization for all uploaded images
  • Soft delete support - images never permanently deleted
  • Factory support for testing with CardImageFactory
  • JSON:API compliant requests and responses
  • OpenAPI documentation for all image endpoints
  • Automatic Thumbnail Generation (#1046)
  • Three thumbnail sizes: small (150w), medium (300w), large (600w)
  • Asynchronous generation using queued jobs (GenerateImageThumbnails)
  • Quality optimization per size: 100% (original), 85%/80%/75% (large/medium/small)
  • Download endpoint supports ?size parameter
  • Graceful failure handling with 3 retry attempts
  • php artisan images:regenerate-thumbnails command for bulk operations
  • Config-driven thumbnail system via config/images.php
  • JPEG Standardization (#1047)
  • All uploaded images converted to JPEG for consistency
  • Smart JPEG handling: existing JPEGs stored as-is (no re-encoding)
  • PNG/WebP conversion at 100% quality
  • Rich variant metadata: width, height, file_size, quality, format
  • Srcset generation for responsive images
  • Sizes attribute recommendations for optimal browser selection
  • CDN Integration (#1048)
  • DigitalOcean Spaces CDN integration for global delivery
  • Versioned URLs with automatic cache invalidation (?v=timestamp)
  • Smart redirect: Download endpoint (302) to CDN instead of proxying
  • Cache headers: 1-year TTL with immutable directive
  • Model methods: cdnEnabled(), getCdnUrl(), getVersionedUrl()
  • Comprehensive CDN setup documentation
  • Fallback support for local disk when CDN not configured
  • Comprehensive Testing (#1063, #1073, #1078)
  • 628 total tests (77 new for Card Images functionality)
  • CardImageRestTest: 29 API endpoint tests
  • CardImageModelTest: 14 model and relationship tests
  • CardImageCdnTest: 13 CDN functionality tests
  • CardImageSchemaValidationTest: 24 JSON:API schema validation tests
  • ImageUploadServiceTest: 21 service layer tests
  • CardImageController tests: 18 authorization and CRUD tests
  • GenerateImageThumbnails tests: 8 job tests
  • 52 Bruno CLI integration tests for Card Images
  • Performance benchmarking deferred to #1079

Changed​

  • Image upload service now sets cache headers on upload to Spaces
  • CardImageTransformer returns versioned CDN URLs in all responses
  • Download endpoint redirects to CDN when enabled
  • All image URLs include version parameter for cache busting
  • Card relationship: ?include=images support in Card API responses

Configuration​

  • New config/images.php for centralized image configuration
  • .env.example includes comprehensive image/CDN variables
  • IMAGE_STORAGE_DISK env variable (local, s3, or spaces)
  • DigitalOcean Spaces configuration (DO_SPACES_*)
  • CDN endpoint configuration (DO_SPACES_CDN_ENDPOINT)

📦 [0.6.1] - November 8, 2025​

Fixed​

  • Fix version script to extract version from commit message for squash-merged releases (#1054)
  • Version script now correctly handles squash merges from release branches
  • Extracts version from "Release version X.Y.Z" commit message pattern
  • Prevents creating incorrect versions (e.g., 0.5.1 instead of 0.6.0)

📦 [0.6.0] - November 8, 2025​

Added​

  • Image UUID field for cards (#1014)
  • New image_uuid field in api_cards table for storing image references
  • Support for UUID-based image naming convention
  • Indexed for efficient lookups
  • Nullable field for backward compatibility
  • Included in Card API responses
  • Factory support with withImage() method for testing
  • Set source tracking table and API (#1013)
  • New api_set_sources table to track data sources for sets
  • Support for tracking checklist, metadata, and images sources
  • Source URL, name, type, and verification timestamp
  • Unique constraint: one source per type per set
  • Cascade delete when parent set is removed
  • Optional sources include in Set transformer
  • Full CRUD API endpoints at /v1/set-sources
  • JSON:API compliant requests and responses
  • Request validation for all operations
  • v2 API endpoints with corrected checklist response structure (#972)
  • New /v2/cards and /v2/sets endpoints
  • Fixed JSON:API compliance issues from v1 endpoints
  • Improved response structure consistency
  • Personal Access Token authentication (#891, #1007, #1009)
  • Simplified authentication for GPT and API clients
  • Documentation for Personal Access Token usage
  • OpenAPI documentation updates
  • Per-client rate limiting for OAuth clients (#1002, #1004)
  • Configurable rate limits per OAuth client
  • Operator documentation for rate limit configuration
  • Enhanced API security and resource management
  • OAuth scope-based authorization (#948, #955, #956, #960, #965)
  • Read scope for data retrieval operations
  • Write scope for create/update operations
  • Delete scope for deletion operations
  • Status filtering based on OAuth scopes
  • OAuth2 Password Grant support (#939)
  • Enable OAuth2 password grant in Passport 13
  • Admin authentication workflows
  • MCP server integration (#949, #1007)
  • Data cleanup workflows via MCP
  • Testing documentation for MCP servers
  • Release discovery slash commands (#986, #987, #991, #992, #993, #994, #995, #996)
  • /discover-new-releases - AI-powered release discovery foundation
  • /add-release - Helper command for single set addition
  • Multi-source web scraping for release discovery
  • Database cross-checking logic
  • Interactive review and confirmation flow
  • Batch set creation with error handling
  • Comprehensive workflow documentation
  • Auto-linking of branches to issues in /issue command
  • Data cleanup slash commands (#916, #930, #932, #975, #976)
  • Interactive set cleanup slash command system
  • /find-missing-oncard (renamed from /find-missing-players)
  • Data-cleanup agent for card and set publication preparation
  • Documentation improvements (#912, #957, #966)
  • Comprehensive database schema documentation
  • Comprehensive OAuth 2.0 documentation
  • Testing and quality improvements
  • Linting workflow for fast code quality checks (#989, #990)
  • Consolidate integration test commands into Makefile (#963)
  • Review and fix skipped tests (#1027, #1029)
  • Protected block system for documentation generation (#826, #1016)
  • Simplify documentation generation workflow
  • CLI flags for selective documentation generation

Changed​

  • Upgraded PHPUnit to 12.4 (#1037)
  • Breaking: PHP 8.3+ now required
  • Migrated PHPUnit test annotations to PHP 8 attributes (#1036)
  • All 491 tests passing
  • Updated all dependencies from Dependabot PRs (#1017, #1032)
  • Laravel, Passport, and other core dependencies updated
  • GitHub Actions dependencies updated
  • All tests passing after updates
  • Consolidated dependency updates (#1052, #1053)
  • Updated lcobucci/clock from 3.3.1 to 3.5.0
  • Updated @usebruno/cli from 2.13.2 to 2.14.0
  • Standardized GitHub Actions to latest versions
  • All tests passing after updates
  • Improved release notes generation (#1023)
  • Include CHANGELOG.md content in release notes
  • Add issue links to release notes
  • Data quality improvements
  • Standardized player names for consistency (#1010)
  • Standardized team names for consistency (#922, #1015)
  • Completed manufacturer data for 14,101 sets (#919, #925)
  • Fixed orphaned cards (#918, #924)
  • Cleanup command for soft-deleted records (#920, #928)
  • UUID primary key optimization (#910)
  • Complete optimization with OPTIMIZE and ANALYZE

Fixed​

  • Fix release action to tag correct commit instead of defaulting to master (#1052, #1053)
  • Added commit parameter to ncipollo/release-action in all release workflows
  • Ensures RC and beta tags point to correct branch commits
  • Fixes "already_exists" error when creating releases
  • Fix /v1/sets endpoint pagination to accept per_page parameter (#1024)
  • Fix Bruno collection issues found during manual API testing (#1019, #1030)
  • Fix API welcome page version display (#943, #1005)
  • Fix markdown linting violations in docs/ folder (#962, #964)
  • Resolve PHP 8.x deprecation warnings in card name generation (#945, #974)

Removed​

  • Deprecated non-versioned API routes (#1025)
  • Removed all non-versioned endpoints
  • All API access now requires /v1/ or /v2/ prefix
  • Deprecated api_card_player_teams table (#923, #929)
  • Data cleanup phase 3 completion
  • Legacy table structure removed
  • Duplicated slash commands migrated to tools repository (#1022)
  • Cleanup of redundant commands

📦 [0.5.0] - November 1, 2025​

🎉 Highlights​

  • Modern Testing Framework: Upgraded to PHPUnit 12.4.2 with PHP 8.3+ support
  • Comprehensive Dependency Updates: 47 packages updated including Laravel 12.36.1 and Passport 13.3.0
  • JSON:API Compliance: Strict header validation enforcement for all v1 endpoints
  • Cleaner API Surface: Removed deprecated non-versioned routes
  • Enhanced Testing: 491 PHPUnit tests + 111 Bruno integration tests, all passing
  • Improved Code Quality: PHP_CodeSniffer 4.0, full markdown linting compliance

Added​

  • JSON:API Accept and Content-Type header validation (#1026)
  • Strict JSON:API specification compliance for all v1 endpoints
  • Content-Type header required on POST/PUT/PATCH/DELETE requests
  • Returns 415 Unsupported Media Type for invalid Content-Type
  • Returns 406 Not Acceptable for invalid Accept parameters
  • CLI flags for selective documentation generation (#826)
  • Team name standardization command (#922)
  • Player name standardization command (#921)
  • Abstract CleanupCommand base class for data quality tools

Changed​

  • Upgraded PHPUnit from 11.5.43 to 12.4.2 (#1034, #1035)
  • Breaking: PHP 8.3+ now required (upgraded from 8.2)
  • Migrated all test annotations to PHP 8 attributes
  • Updated 4 test files with 47 test methods
  • Eliminates all 50 PHPUnit deprecation warnings
  • All 491 tests pass, 111 Bruno integration tests pass
  • Updated all dependencies (#1017, #1032)
  • GitHub Actions (5 updates): upload-artifact v5, download-artifact v6, setup-node v6, git-auto-commit v7, github-script v8
  • PHP/Composer (38 updates): Laravel 12.36.1, Passport 13.3.0, webpatser/laravel-uuid 6.2.0 (major), PHP_CodeSniffer 4.0 (major)
  • Node.js/npm (3 updates): @usebruno/cli 2.13.2, dotenv 17.2.3, axios 1.13.1
  • All 491 PHPUnit tests pass after updates
  • Replace Route::resource() with Route::apiResource() for all v1 endpoints (#1019)
  • Removes unnecessary create and edit routes (for HTML forms, not APIs)
  • Reduces routes from 7 to 5 per resource
  • Improve release notes generation to use CHANGELOG.md content (#830)
  • Simplified documentation generation to API reference only (#826)
  • Removed hardcoded version numbers from auto-generated docs
  • README.md and guide docs are now regular markdown files
  • Reduces documentation churn on version bumps

Removed​

  • Deprecated non-versioned API routes (#1021)
  • Breaking: All endpoints now require version prefix (e.g., /v1/cards)
  • Removed 12 legacy controllers
  • Removed DeprecatedRoute middleware
  • Removed 13 deprecated test files
  • Unused newman package and 64 transitive dependencies (#1017)
  • Project now uses Bruno CLI exclusively for API testing
  • Reduces npm package count by 65 packages
  • Reduces npm security vulnerabilities from 7 to 4
  • Migrated data quality slash commands to tradingcardapi-tools repository (#1020)

Fixed​

  • /v1/sets endpoint pagination (#997)
  • Accept both per_page (JSON:API standard) and limit (legacy)
  • Add validation to enforce maximum of 100 items per page
  • Resolves MCP server token limit issues
  • Fix 56 duplicate Bruno request files (#1019)
  • Fix broken validation script in 32 Bruno request files (#1019)
  • Fix Attribute and ObjectAttribute url() methods missing API version prefix (#1019)
  • Review and fix all skipped tests - reduced from 4 to 0 (#1027)
  • Fix all markdown linting issues (#826)

Security​

  • OAuth2 security improvements via Laravel Passport 13.3.0
  • JWT security improvements via lcobucci/jwt 5.6.0
  • Symfony component security patches (11 packages)

Testing​

  • 491 PHPUnit tests - all passing with PHPUnit 12.4.2
  • 111 Bruno integration tests - all passing
  • Zero skipped tests - all problematic tests fixed
  • PHP_CodeSniffer 4.0 - all 320 files pass
  • Full markdown linting - all docs compliant

📦 [0.4.23] - October 18, 2025​

Added​

  • Interactive set cleanup slash command system for data quality management (#930, #932)
  • Cleanup command for soft-deleted records with configurable retention periods (#920, #928)
  • Data-cleanup agent for card and set publication preparation with automated quality checks (#916)
  • Comprehensive database schema documentation with entity relationships and data quality metrics (#912)

Changed​

  • Enabled OAuth2 password grant in Passport 13 for admin authentication support (#939)
  • Complete UUID primary key optimization with OPTIMIZE and ANALYZE for improved query performance (#910)
  • Complete manufacturer data for 14,101 sets using automated matching and assignment (#919, #925)

Removed​

  • Deprecated api_card_player_teams table as part of Data Cleanup Phase 3 (#923, #929)

Fixed​

  • Fix orphaned cards without valid set relationships (#918, #924)

📦 [0.4.22] - October 12, 2025​

Fixed​

  • Fix checklist card ordering to use natural sort (Issue #904)

📦 [0.4.21] - October 12, 2025​

Added​

  • Add Bruno test files for format parameter
  • Add compact format parameter to checklist endpoint (Issue #901)

📦 [0.4.20] - October 12, 2025​

Removed​

  • Remove release_date from OpenAPI spec (Issue #899)

Fixed​

  • Fix: Add validation for order_by parameter to prevent 500 errors (Issue #899)

📦 [0.4.19] - October 12, 2025​

Fixed​

  • Fix: Make Accept header optional for API calls (Issue #897)

📦 [0.4.18] - October 12, 2025​

Fixed​

  • Fix: Replace Card::all() with paginated query to prevent memory exhaustion
  • Fix: Remove expensive user authentication from rate limiter
  • Fix: Increase PHP memory limit to 512M to resolve Personal Access Token authentication failures

📦 [0.4.17] - October 12, 2025​

Added​

  • docs: Add Personal Access Token support to Bruno collection

Fixed​

  • fix: Make Personal Access Token test skip gracefully when not configured
  • fix: Correct middleware naming to resolve test failures
  • hotfix: Enable Personal Access Token authentication for API endpoints

📦 [0.4.16] - October 11, 2025​

Added​

  • Add 'read' OAuth scope for ChatGPT GPT integration (Issue #888)

📦 [0.4.15] - October 11, 2025​

Fixed​

  • Fix version script and code style issues
  • Fix: Use static return type instead of self for withParameters
  • Fix: Add default parameter and return type to withParameters
  • Fix: Implement missing withParameters method in AuthorizationViewResponse
  • Hotfix: Bind Passport AuthorizationViewResponse for OAuth authorization flow (Issue #885)

📦 [0.4.14] - October 3, 2025​

Fixed​

  • Fix card attribute clearing when using object-attributes relationships format

📦 [0.4.13] - October 3, 2025​

Added​

  • Implement container separation for API project local development

Changed​

  • Finalize API makefile environment variable handling

Fixed​

  • Fix upd command to properly handle environment variables

📦 [0.4.12] - October 3, 2025​

Fixed​

  • Fix test failure in CardRestTest::testExtraAttributesClear
  • Fix markdown linting errors in DATASET-MANAGEMENT.md
  • Fix card attribute assignment bug (Issue #874)

📦 [0.4.11] - October 2, 2025​

Changed​

  • github-actions(deps): bump actions/github-script from 7 to 8

Fixed​

  • hotfix: Fix version automation infinite loop (#872)

📦 [0.4.10] - October 1, 2025​

Changed​

  • Trigger CI after cleaning up test tags

📦 [0.4.9] - October 1, 2025​

Added​

  • Add Dataset Management documentation to README
  • Add snapshots directory to dockerignore and clean up orphaned files
  • Add fast backup/restore system with shell scripts
  • Add comprehensive background dataset import system
  • feat: implement lightning-fast database switching for dataset management
  • docs: add comprehensive dataset management documentation
  • feat: implement dataset management system for development/testing

Changed​

  • Update DATASET-MANAGEMENT.md to remove obsolete sections
  • Simplify dataset management system by removing database switching
  • Update dataset management documentation with backup/restore system
  • Exclude Docker storage directory from builds and git
  • Simplify dataset management system to single database approach

Removed​

  • Remove references to non-existent dataset:reset command
  • Remove cross-project integration and clean up dataset configuration
  • Remove obsolete ImportProductionDatasetJob
  • Remove dataset API endpoint and controller
  • Remove obsolete DatasetImportBackgroundCommand
  • Remove obsolete DatasetForceRestartCommand
  • Remove obsolete DatasetCleanupCommand and scheduled cleanup
  • Remove legacy snapshots directory and references

Fixed​

  • fix: exclude dataset snapshots from Docker build context

📦 [0.4.8] - September 19, 2025​

Added​

  • Add set_id field to Card API response

📦 [0.4.7] - September 18, 2025​

Fixed​

  • Fix JSON escaping in trigger-docs-update.yaml workflow

📦 [0.4.6] - September 18, 2025​

Added​

  • Add workflow to trigger documentation updates on releases

Changed​

  • Trigger CI after cleaning up test tags

📦 [0.4.5] - September 6, 2025​

Added​

  • Add token fallback for Dependabot PRs

Changed​

  • npm(deps-dev): bump @usebruno/cli from 1.40.0 to 2.9.1

📦 [0.4.4] - September 5, 2025​

Added​

  • Add commit deduplication to changelog script
  • Add token fallback for Dependabot PRs

Changed​

  • github-actions(deps): bump actions/download-artifact from 4 to 5

📦 [0.4.3] - September 5, 2025​

Changed​

  • Use actual committer's name and email for automation commits
  • Use DEPLOY_APP_TOKEN to trigger status checks on automation commits
  • Replace manual git commands with git-auto-commit-action
  • Improve auto-version-bump exclusion logic
  • Disable auto-version-bump for workflow development branches
  • Restore content to empty 0.4.3 changelog section

Fixed​

  • Fix git-auto-commit-action configuration issues

📦 [0.4.2] - September 5, 2025​

Added​

  • Document new automated PR version management system

Changed​

  • Update CLAUDE.md with new automated PR workflow documentation
  • Start fresh: remove entire 0.4.2 section for clean automation test
  • Resolve changelog conflict - accept clean automated version
  • Re-enable automation for all release PRs including workflow development
  • Automate version bump workflow for release PRs

Removed​

  • Remove useless automatic version updates comment from workflow
  • Remove manual changelog entry to let automation handle everything
  • Remove outdated manual workflow instructions from PR comments

Fixed​

  • Fix PR status checks by forcing CI run on final commit
  • Fix all remaining markdown linting issues in documentation
  • Fix corrupted changelog structure after merge conflict
  • Final fix: disable auto-version-bump for workflow development branches

📦 [0.4.1] - September 5, 2025​

Changed​

  • Updated GitHub Actions dependencies
  • Bump actions/checkout from v4 to v5 (Dependabot)

📦 [0.4.0] - September 5, 2025​

🔒 Major Security & Rate Limiting Features​
  • Comprehensive Rate Limiting System with Redis backend and tiered limits (Issues #678, #680, #681)
  • Anonymous rate limits: 100 requests/hour
  • Authenticated rate limits: 2000 requests/hour
  • OAuth client rate limits: 1000 requests/hour
  • OAuth token endpoint: 10 requests/minute
  • Rate limit headers in all responses
  • Comprehensive test suite with performance and edge case testing
📊 API Usage Analytics & Monitoring​
  • Comprehensive API usage logging system for rollout analytics (Issue #735)
  • ApiUsageLogging middleware for automatic request tracking
  • LogApiUsage job for asynchronous database logging
  • Database migration for api_usage_logs table with optimized indexes
  • Complete test coverage with 20 test cases (unit and feature tests)
  • Analytics queries for endpoint popularity, response times, and error rates
  • Endpoint normalization for ID and UUID parameters
🔧 Infrastructure & Dependencies​
  • Updated all PHP and Node.js dependencies to latest secure versions (Issue #791)
  • Laravel framework 12.21.0 → 12.28.1
  • Laravel Passport 13.0.6 → 13.2.0
  • PHPUnit 11.5.27 → 11.5.36
  • Node.js 22 → 23
  • Security fixes for @usebruno/cli and newman packages
  • Updated GitHub Actions to latest versions

📚 Enhanced Documentation & Testing​

  • Comprehensive rate limiting documentation across all relevant files
  • Manual rate limiting testing command (php artisan test:rate-limiting)
  • Bruno CLI integration tests for rate limiting scenarios
  • Troubleshooting procedures and debugging guides
  • Enhanced documentation generation with preserved manual content
  • Fixed documentation template formatting issues

⚠️ Breaking Changes​

  • New rate limiting may affect high-volume API consumers:
  • Anonymous users now limited to 100 requests/hour (previously unlimited)
  • OAuth token generation limited to 10 requests/minute
  • New rate limit headers in all responses

🐛 Bug Fixes​

  • Resolved API usage logging content removal during version bumps

📦 [0.3.9] - August 13, 2025​

Fixed​

  • Documentation workflow not triggering when workflow file itself is updated

📦 [0.3.8] - August 12, 2025​

Fixed​

  • Documentation generation workflow references to non-existent docs/generated/ directory

📦 [0.3.7] - August 12, 2025​

Fixed​

  • Documentation generation workflow failing due to incorrect npm script name

📦 [0.3.6] - August 12, 2025​

Fixed​

  • OAuth key file ownership issues causing "Key path does not exist or is not readable" errors (Issue #689)

📦 [0.3.5] - August 12, 2025​

Fixed​

  • OAuth client authentication failures with "This password does not use the Bcrypt algorithm" error (Issue #675)
  • Laravel Passport 13 client secret validation by updating plain text secrets to bcrypt format
  • Production API authentication issues for OAuth clients with legacy plain text secrets
  • Docker builds failing on Debian Trixie due to nginx.org repository compatibility
  • Integration test failures by adding HTTP support alongside HTTPS in nginx configuration

Added​

  • oauth:fix-client-secrets artisan command to safely update OAuth client secrets to bcrypt format
  • Database migration to automatically fix OAuth client secret format during deployment
  • Dry-run mode for OAuth client secret fix command with safety confirmations

📦 [0.3.4] - August 8, 2025​

Added​

  • Comprehensive PROJECT_OVERVIEW.md markdown description file with strategic business and technical analysis (Issue #648)

📦 [0.3.2] - August 8, 2025​

Fixed​

  • Version script master branch logic simplified to use reliable git tag-based versioning instead of fragile commit message parsing (Issue #666)

📦 [0.3.1] - August 8, 2025​

Added​

  • Comprehensive version script test suite in build/tests/ with validation for all branch scenarios
  • Version script validation integrated into lint checks (npm run lint:version)
  • ./build/scripts/test-version unified test runner for version script validation
  • Improved npm test scripts: npm test now runs PHP tests and version script validation (CI-safe)

Changed​

  • Update CHANGELOG.md for version script hotfix
  • Auto-update OpenAPI documentation

Fixed​

  • Fix version script tests for CI environments
  • Fix version script tag selection to prefer higher version numbers
  • Fix version script conditional logic bug and add comprehensive tests
  • Critical version script bug: Fixed conditional logic in build/version.sh where line 61 was overwriting release branch version extraction (Issue #662)
  • Version synchronization between release branches and actual release versions
  • Release branch version extraction now properly uses branch names (e.g., release/0.3.0 → 0.3.0)

📦 [0.3.0] - August 7, 2025​

Added​

  • Bruno API testing collection with comprehensive endpoint coverage and CI/CD validation
  • OAuth integration tests with automatic token management and authentication flow validation
  • Complete OpenAPI 3.0 documentation with interactive Swagger UI
  • OpenAPI schema definitions for all API resources in /app/OpenApi/Schemas.php
  • L5-Swagger integration for automatic documentation generation
  • GitHub Actions workflow for automated documentation validation and deployment
  • Makefile commands: make docs, make docs-validate, make help
  • Public documentation directory /public/docs/ with usage examples
  • V1 namespaced controllers with complete OpenAPI documentation for versioned endpoints
  • Complete documentation generation system with automated repository documentation from OpenAPI specifications

Changed​

  • All API endpoints now include comprehensive OpenAPI attributes
  • Updated L5-Swagger configuration for optimal documentation generation
  • Enhanced README with OpenAPI documentation section
  • V1 routes now use dedicated V1 controllers in App\Http\Controllers\V1 namespace
  • OpenAPI documentation includes both legacy and V1 versioned endpoints with separate tags
  • make changelog-update command now also updates OpenAPI documentation to ensure version consistency
  • User-friendly development scripts with comprehensive output and error handling moved to build/scripts/
  • Removed obsolete gulp build system, Postman tooling, and legacy file references for simplified development workflow

📦 [0.2.85] - July 27, 2025​

Added​

  • New make release-notes-preview command to preview GitHub release notes before merging
  • Enhanced GitHub release notes with clean, organized format and quick navigation
  • Dedicated "Issues Fixed" section in release notes with clickable GitHub issue links
  • Claude API integration for intelligent release summary generation based on commits, file changes, and changelog content

Changed​

  • GitHub release notes now use curated changelog content instead of raw commit messages
  • Release notes structure improved with sections for Summary, What's Changed, Links & Resources, and Deployment
  • Docker image links now included directly in release notes for easy access

📦 [0.2.84] - July 26, 2025​

Added​

  • Added make changelog-update command to automatically organize changelog with next version
  • Added make changelog-preview command to see next version and current unreleased changes
  • Added changelog requirements to pull request template
  • Added --master flag to version script to simulate master branch version

Changed​

  • Modified build process to remove automatic changelog commits during merge conflicts
  • Updated changelog maintenance to manual process with make commands for organization
  • Updated documentation (CLAUDE.md and VERSION_MANAGEMENT.md) to explain changelog workflow
  • Removed automatic changelog commits from build workflow entirely

Fixed​

  • Resolved merge conflict issues caused by automated changelog updates
  • Fixed recursive changelog generation that included meta-commits

📦 [0.2.83] - July 27, 2025​

Fixed​

  • fix: modify how changelog gets updated

📦 [0.2.82] - July 27, 2025​

Changed​

  • chore: cleanup changelog entries

📦 [0.2.81] - July 27, 2025​

Added​

  • API versioning strategy with URL path versioning (/v1/)
  • Version information endpoint (/version)
  • Dynamic version generation from Git using build/version.sh
  • Synchronized versioning across application, API, and CI/CD
  • Comprehensive changelog documentation

📦 [0.2.80] - July 27, 2025​

Fixed​

  • fix: fix the release process

📦 [0.2.79] - July 27, 2025​

Fixed​

  • fix: fix docker image tagging process

📦 [0.2.78] - July 27, 2025​

Added​

  • feat: add API versioning with v1 endpoints
  • #611 Add tests for new routes
  • #611 Add versioning support to the api

Changed​

  • Merge pull request #616 from cardtechie/feature/611-change-versioning
  • #611: Automate document updates
  • Merge branch 'master' into feature/611-change-versioning
  • #611 Dynamically figure out the version

Fixed​

  • Fix: Fix process with generation of version number
  • #611 Fix failing tests on github

📦 [0.2.73+] - January 26, 2024​

Added​

  • Pre-release API with comprehensive trading card management
  • Core endpoints: cards, sets, players, teams, genres, years, manufacturers, brands
  • OAuth2 authentication with Laravel Passport 13
  • JSON:API specification compliance
  • UUID primary keys for all models
  • Polymorphic OnCard relationships (Players, Teams, PlayerTeams)
  • Automatic card name generation with format tokens (%d, %o, %l, %a, %s)
  • Soft delete functionality via Laravel observers
  • Docker containerization with multi-environment support
  • OpenAPI/Swagger documentation
  • Comprehensive test suite (PHPUnit, linting, code standards)
  • GitHub Actions CI/CD pipeline

Technical Details​

  • Framework: Laravel 12 with PHP 8.2+
  • Database: MySQL 8.0 with UUID primary keys
  • Authentication: Laravel Passport 13 OAuth2
  • API Standard: JSON:API specification compliance
  • Testing: PHPUnit 11, PHP CodeSniffer, OpenAPI validation
  • Containerization: Docker with development, testing, and production environments

Endpoints​

  • GET|POST /cards - Card management
  • GET|POST /sets - Set management with checklist support
  • GET|POST /players - Player management
  • GET|POST /teams - Team management
  • GET|POST /playerteams - Player-team relationship management
  • GET|POST /attributes - Card attribute management
  • GET|POST /object-attributes - Object attribute relationships
  • GET|POST /genres - Genre management with soft delete support
  • GET|POST /years - Year management
  • GET|POST /manufacturers - Manufacturer management
  • GET|POST /brands - Brand management
  • GET /stats/{type} - Statistics endpoint

Security​

  • OAuth2 client credentials flow
  • Environment-based configuration
  • SSL/TLS support with Let's Encrypt
  • No sensitive data in repository

📊 Release Statistics​

  • Latest Stable: 0.11.0
  • Total Releases: 286+ releases since inception
  • Release Frequency: Weekly updates with patches, monthly for minor versions

🔗 Additional Resources​

📖 API Documentation​

🆘 Help & Feedback​

🛠️ Development Tools​

🏷️ Version Information​

  • Current API Version: 0.11.0
  • API Base URL: https://api.tradingcardapi.com
  • Release Schedule: Continuous deployment with semantic versioning
  • Support Policy: Latest 3 major versions supported

🔔 Stay Updated​

📧 Get Notified​

💬 Support​


Questions About a Release?

Have questions about a specific API change or need help upgrading? We're here to help! Reach out through email support.


Automated Updates

This changelog is regularly updated with the latest API releases. Check back here for the newest changes, or subscribe to API updates.


Last updated: October 11, 2026 at 03:45 AM UTC