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/forgotmints a single-use expiring opaque token and emails a portal reset link, andPOST /v1/user/password/resetredeems it, sets the new password, and revokes all of the user's Passport sessions. forgotalways 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 newRATE_LIMIT_PASSWORD_FORGOTlimiter, and the reset email needsLOOPS_RESET_PASSWORD_TEMPLATE_IDset 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 theinternal-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-keysto revoke them as part of the reset when an account may be compromised. - [Issue #1916] Add a
back-merge-stable-to-developGitHub Actions workflow that back-mergesmasterintodevelopon every push tomaster, pushing directly when the merge is clean and opening a conflict PR otherwise, sodevelopnever silently falls behind the stable branch. - The sync is one-way by design:
develop->masterpromotion stays a deliberate release step and is not automated. - [Issue #2431] Add a read-only
catalog:doctorartisan 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-unfilteredmode toverification:compare-gatethat 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.mdthat 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 themerge_grouptrigger the required test contexts need. - Enabling the queue itself is an operator settings change; the ADR carries the checklist, and the
merge_grouptrigger must be merged tomasterbefore 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.34stable releases back intodevelopacross three manual back-merges, resolving the rate-limiter, internal-route,CHANGELOG.mdandbuild/scripts/test-build-scriptsconflicts as unions. - Development-line housekeeping only: every change these syncs carried already shipped on
masterin its own0.10.xrelease.
Fixed
- [Issue #2253] Stamp the released version into the served OpenAPI spec at image build time and correct public
/api/v1references to/v1. - The committed
public/docs/openapi.jsondeliberately keeps its0.0.0-devsentinel; 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
::warninginstead 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
mastersync 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
TokenGuardreports League'sOAuthServerException, not the Passport wrapper listed in$dontReport; the handler now suppresses client-caused (4xx) OAuth failures while still reporting genuine 5xxserver_errorcases. - [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.*orvariants[].urlbeyond the TTL must re-fetch the record. SetIMAGE_SIGNED_URLS_ENABLED=falseto restore unsigned URLs, and runphp artisan images:backfill-visibilityto re-ACL objects already written public-read.
📦 [0.10.113] - October 10, 2026
Added
- [Issue #2730] Add an internal-scope
GET /internal/analytics/customer-usageendpoint reporting per-customer request counts over a date range.
Fixed
- [Issue #2657] Reject any
api_sets.statusvalue outsidedraft/publishedwith a DB-level constraint and a model-level guard. - [Issue #2670] Exclude soft-deleted player-teams from
/v1/player-teamsby addingSoftDeletestoPlayerTeam. 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
actionlintCI job tolint.yamlso 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.pathskey insync-labels.yaml, one of only two pre-existingactionlintfindings in the repo.
📦 [0.10.110] - October 9, 2026
Fixed
- [Issue #2616] Skip tagging, building, and publishing a release when a push to
mastercollates nochangelog.d/fragments, so release-tag numbers stay in lockstep withCHANGELOG.mdsections. - 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
MOUNTSdocker-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
MOUNTSargument list only; the markdownlint mounts and the targeted-phpunit--filterstill 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 localmake test-mysqltarget is also available.
📦 [0.10.107] - October 8, 2026
Added
- [Issue #2562] Add
.claude/version-bump.mddeclaring 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/statsendpoints by classifying docs per-operation on explicitsecurity: []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
VERSIONvalues throughenv:instead of interpolating them directly intorun:shell commands, closing a script-injection surface in the release workflows. - Defense-in-depth only —
VERSIONis 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()'sdeleted_atfilters, 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-*.keyfiles 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()returnsinvite_invalid, not a distinct "revoked" error.
Security
- [Issue #2557] Pin
nanoid,@faker-js/faker, andaxiosto 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-gateper 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-expirescrashing after issuing the invite, caused by.binding tighter than?:in theExpires:report line.
📦 [0.10.99] - October 2, 2026
Fixed
- [Issue #2655] Maintain
published_visiblefor 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
pcntlPHP extension in the app image soqueue: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.confsets nostopwaitsecs, 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 forRebuildVisibilityForSetby #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
maintenancequeue and single worker so long visibility rebuilds no longer block customer-facing jobs. - Takes effect on the next image deploy; rebuilds already queued on
defaultbefore the deploy still drain there.
📦 [0.10.94] - October 1, 2026
Fixed
- [Issue #2642] Add the
lowqueue to the production worker's queue list so backloggedLogApiUsagejobs are consumed instead of growing unboundedly. defaultkeeps strict priority —lowis only polled whendefaultis empty, so this does not delay current-traffic jobs.
📦 [0.10.93] - October 1, 2026
Fixed
- [Issue #2654] Maintain
published_visiblesynchronously when a card or on-cardable row is attached to an already-published set. - The maintenance is a bounded, indexed
UPDATEon the affected rows only — it never dispatches the catalog-wideRebuildVisibilityForSetjob.
📦 [0.10.92] - September 30, 2026
Added
- [Issue #2665] Add
verification:compare-gateand 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 arun: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
RebuildVisibilityForSetmid-run: thedatabaseconnection'sretry_after(90s) was far below the job's real runtime (13m17s observed), soDatabaseQueuetreated the row as expired and the second worker ran a concurrent copy of the same job, leaving thedefaultqueue with no spare capacity. retry_afteris now 1860s, the worker runs with--timeout=1800, and the job declares$timeout = 1800plus 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 thejobstable so Laravel's queue-pop query stops full-scanning the primary key: the pop forqueue=defaultmeasured 6.2s in production against a 1.2ms baseline, draining roughly one job every three seconds. - Deploy note: stop the
laravel-workerprogram before migrating and restart it after — theALTERneeds 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-keyso 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/cardsis a documented exception to the uniformlike:rule:filter[name]=like:…returns a 400, because the leading-wildcardLIKEthe 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=setsbehavior.
📦 [0.10.84] - September 26, 2026
Fixed
- [Issue #2559] Report the per-billing-account plan window in
GET /v1/user/usagealongside thetc_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.shstays 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]toGET /v1/cardsand aGET /v1/teams/{team}/cardsconvenience 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-visibilitycompletes 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-visibilityrebuild ofapi_player_teamsno 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/teamsand/v1/player-teamsfilter on the materialisedpublished_visiblesignal 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
falseon the host disables the gate until the next deploy, and the durable change is editing the compose default and cutting a release. The nightlycatalog:rebuild-visibilitybackstop 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
mastermoves 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
mastertest suite by implementingInviteGate::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
--previewand--sleepoptions tocleanup:backfill-set-identityso the draft-scope backfill can be reviewed in full before it is applied and paced when it is. --previewis 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=compactreturns, not three.
📦 [0.10.73] - September 10, 2026
Fixed
- [Issue #2448] Remove the
github.tokencomposer-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/resendso a released hold cannot be redeemed later and exceed the invite'smax_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_exhaustedand its account is rolled back;InviteGate::claim()returnsboolinstead ofvoid. - [Issue #2534] Accept the
like:operator prefix on the/v1/playersfirst_name,last_nameandfull_namefilters 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 aread:publishedtoken. - 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/snapshotsand/v1/stats/growthpublic so aggregate marketing data no longer depends on a credential. - Anonymous callers receive the existing published-only view (
totalequalspublished,draftis 0); rate limiting still applies and API usage logging is skipped.
Fixed
- [Issue #2535] Emit relationship
links.selfandlinks.relatedwith the/v1/version segment instead of dropping it. - Response bytes change for any resource whose transformer supplies no custom
linksblock; its top-levelselfbecomes 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/playersand/v1/setsin favor of one bracketedfilter[...]convention across v1. - Bare spellings keep working for at least one release and now return
DeprecationandSunsetresponse 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/countsas 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/signupreturns 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_entriestable 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:revokeartisan 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 theInviteGatecontract; 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 developconflict PR onto the CardTechie project board at👀 In review, with asynclabel, so it enters the normal work queue instead of living only in the PR list. - Best-effort: a
PROJECT_TOKENwithoutprojectscope still opens the PR, logs a::warning, and exits 0.
📦 [0.10.61] - September 7, 2026
Security
- [Issue #2383] Gate
/v1/players,/v1/teamsand/v1/player-teamson a materialisedpublished_visiblecolumn 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 productionEXPLAINand unboundedCOUNTevidence #2373 requires has been captured withphp artisan status-gate:measure-on-cardable.
📦 [0.10.60] - September 7, 2026
Fixed
- [Issue #2355] Gate the develop beta build on
validate-changelog-readybefore 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.deletedevent 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-subscriptionrepairs 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-brandscommand 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 --setsmust 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-identitycommand. - Rendered set names change for affected rows (e.g.
1996-97 Black Diamondbecomes1996-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-attributesvisibility from the publication status of the set each attribute resolves to. - Off by default: set
STATUS_GATE_OBJECT_ATTRIBUTES_ENABLED=trueonly after recording the production measurement fromphp artisan status-gate:measure-object-attributeson 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_imagesheld 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;
internalandread:all-statuscallers are unaffected. SetSTATUS_GATE_TAXONOMY_ENABLED=falseto 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 eachapi_usage_logsrow 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-sourcesby the publication status of the owning set so draft sets' provenance records are no longer readable by customer-grade tokens. - Tokens holding
writeordeletewith onlyread:publishednow receive a 404 instead of mutating or deleting a draft set's sources;internalandread:all-statustooling 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/CODEOWNERSif one is wanted.
📦 [0.10.48] - September 4, 2026
Fixed
- [Issue #2439] Declare the
like:filter prefix in the OpenAPI spec, add ameta.scopesignal and a?scope=allenumeration parameter toGET /v1/sets, and escape%and_inlike:filter values. - Behavior change:
?name=like:%no longer returns the whole catalog — use?scope=allinstead.
📦 [0.10.47] - September 4, 2026
Fixed
- [Issue #2468] Correct the v1 checklist OpenAPI description to list all five fields
format=compactreturns, 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/mewith a feature test and correct the last pre-cutover sentence in theDELETE /v1/meOpenAPI 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 fromtrial_ends_atfor accounts that already hold a ledger, so no existing trial is re-notified on deploy. - [Issue #2504] Tolerate the legacy
pageNamequery parameter on allowlistedv1list endpoints, restoring/app/sets*on cardalmanac.com after strict query-parameter validation began rejecting it in 0.10.15. pageNameis accepted but deliberately left undocumented and out of rejection messages; it is a compatibility shim to be removed oncecardtechie/tradingcardapi-sdk-phpstops sending it.
📦 [0.10.44] - September 3, 2026
Fixed
- [Issue #2438] Make
format=compacton the v2 set checklist actually reduce response size by dropping the JSON:API envelope. - Compact v2 responses no longer emit
included, per-cardrelationships, or per-resourcelinks; pass an explicitincludeto opt back in.
📦 [0.10.43] - September 3, 2026
Fixed
- [Issue #2374] Extend the published/draft status gate to card images so
/v1/card-imageslist, 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; setSTATUS_GATE_CASCADE_ENABLED=falseto 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.tokenwhensecrets.COMPOSER_TOKENis unavailable, and exempt Dependabot from the changelog-fragment gate it cannot satisfy. - Dependabot runs use a separate secret store, so the empty
COMPOSER_TOKENmadecomposer installfail 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
mastertest suite:UserUsageTestasserted thatGET /v1/user/usagewas throttled on the anonymousapitier, which stopped being true when #2342 moved the route onto its ownthrottle:usagelimiter, breakingUnit Testson every open pull request. - Test-only. The assertion now references
RouteServiceProvider::USAGE_RATE_LIMIT_PER_MINUTEinstead 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_TOKENbreaks the release visibly rather than silently stalling the public changelog. - The check tests the status as a 2xx class:
POST /repos/{owner}/{repo}/dispatchesreturns 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 readGET /v1/user/usagewithout 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_partyroutes that previously answered a data key with 401 now answer 403FORBIDDEN_NOT_INTERACTIVE_SESSION; clients keying off the status code must be updated. - [Issue #2343] Emit
X-RateLimit-Reseton every throttled response and expose theX-RateLimit-*headers through CORS so clients can derive their own usage window. - The
throttlemiddleware alias now resolves toThrottleRequestsWithReset; 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/usagewindow against the limitthrottle:apiactually 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-mutationand@production-impacttags. - [Issue #2427] Add
brandandmanufacturerfilters toGET /v1/setsand expose the brand-to-sets relationship as an opt-ininclude=setson/v1/brands.
📦 [0.10.35] - September 2, 2026
Fixed
- [Issue #2425] Point the API's advertised
changelog_urland 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_DSNrepository 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-
syncQUEUE_DRIVER(notQUEUE_CONNECTION) with a running worker and aqueue:prune-failedschedule, or the send stays on the request path and the oracle remains open. LOOPS_TIMEOUT,LOOPS_CONNECT_TIMEOUT,BREVO_TIMEOUTandBREVO_CONNECT_TIMEOUTare optional repository variables (5s/2s defaults, supplied by prod compose); set one only to retune a bound during an incident, never to0.- 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 (
1e400casts toINF, which Guzzle converts to an unbounded0) 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-statusis 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:rollbackno longer fails on SQLite.
📦 [0.10.30] - August 28, 2026
Changed
- [Issue #2383] Record that
/v1/players,/v1/teamsand/v1/player-teamsare 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_KEYthrough the production deploy path and report it as a readiness capability so founding-interest enrolment stops silently no-opping. - Requires a
PROD_BREVO_API_KEYGitHub 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_CAPthrough the production deploy pipeline and add aFOUNDING_GATE_ENABLEDflag, 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_ENABLEDrepository 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_DRIVERis now pinned toslackin the production compose file so an operator can switch it tologornullwithout a release;OPS_SLACK_WEBHOOK_URLis declared runtime, and the staleOPS_ADMIN_EMAILentry 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 toconfig/now fails CI until it is classified inconfig/operator-tunable.phpas 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_EMAILandconfig('mail.admin_address'); production must setOPS_SLACK_WEBHOOK_URLor 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 tokenwhen 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/cardstotals drop sharply, and internal tooling must holdinternal,read:draftorread:all-statusto 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_atcolumn tobilling_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/countsinstead of a hardcoded zero.
📦 [0.10.18] - August 22, 2026
Changed
- [Issue #2340] Commit each release's
CHANGELOG.mdsection straight tomasterinstead of opening achore/changelog-<version>PR nobody could merge. - The PR mechanism failed for an unforeseen reason: those PRs are markdown-only, and
build-and-test.yamlskipped markdown-only PRs, so the requiredUnit Tests/Integration Testscontexts never reported and not one was ever mergeable. An unmerged changelog PR never lands itschangelog.d/fragment deletions, so each release re-collated its predecessor's fragments under its own version. Ten releases compounded that way, leavingmasterdocumented 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 inbuild/version.sh, so the sync PR thatback-merge-stable-to-developopens fromauto/sync-master-to-developno 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.billinghealth key, abilling:doctorArtisan preflight, and a loud deploy-time warning. - Advisory only: a degraded billing config reports
degradedand never changes the/healthstatus 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-developworkflow tomasterso its push-to-mastertrigger can actually fire, since push-triggered workflows are resolved from the pushed branch's own tree and the file previously existed only ondevelop.
📦 [0.10.11] - August 21, 2026
Fixed
- [Issue #2307] Fix
GET /v1/players/{id}/cardsandfilter[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 atper_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_KEYin 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 ofhttps://api.tradingcardapi.com/api/v1— a path that 404s. The versioned routes are mounted underAPI_PREFIX, which is empty in every deployed environment, so the correct base ishttps://api.tradingcardapi.com/v1. The page now derives the path fromconfig('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/v1path is also corrected inREADME.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/signupfor the same email is accepted again on a single-use invite. The gate previously rejected it withinvite_exhaustedbefore reaching the claim it already held, so an invitee whose verification email never arrived had no way back — the sign-up form refused them andPOST /v1/signup/resendneeds 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/validatenow rejects a code that stops being usable between its verdict and the row read, instead of answering200 validwith a null label for an invitePOST /v1/signupwould turn away.POST /internal/invitesaccepts an explicitcode: nullas "mint a fresh code", matching thenullablethe 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.yamlso the value raised for the launch survives deploys. It had only ever been set by hand on the production host, wherebuild/deploy.shrewrites the compose.envfrom scratch and re-copies the compose file on every deploy — silently revertingPOST /v1/signupto 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/signupandPOST /v1/signup/verifyaccepting credentials from the query string. Both validated and read throughRequest::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, andPOST /v1/signup/verify?data[attributes][token]=…completed a full verification — email verified, founding slot claimed, trial started, and the one-timetc_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 inReferer. Both endpoints now validate and read from the JSON body only, matchingPOST /v1/signup/resend,POST /v1/invites/validateandPOST /internal/invites, which already did.
📦 [0.10.5] - August 16, 2026
Security
- [Issue #2282] Require a valid invite code on
POST /v1/signupso 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_codeis now required, and the shared cohort code must be seeded withphp artisan invite:issueat 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 tomasteris 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
masterafter 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.2and0.10.3tags 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/resendendpoint so a user whose verification link has expired can request a new one from the/verifylanding 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
202response 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 with422 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 tophpcs.xmlso 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-secretandoauth:client-metadatacommands 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-keyendpoint that revokes the authenticated user's opaquetc_live_API key without minting a replacement; returns204 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-tokensartisan command to detect and strip duplicate-adjacent manufacturer tokens from set display names; pass--dry-runfor 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-customerscommand that reports (and, with--delete, removes) Stripe customers orphaned byBillingAccount::forUser()concurrent-loser races. - Report-only by default and on the schedule; the destructive live delete runs only with the explicit
--deleteflag. - [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 forLOOPS_API_KEY,PORTAL_URL, and theLOOPS_*_TEMPLATE_IDids. - Also adds the missing
LOOPS_CHANGE_EMAIL_TEMPLATE_IDto.env.exampleand indexes the operations runbooks indocs/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 newconfig('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_endontobilling_accounts.trial_ends_atfrom the subscription webhook. - [Issue #1973] Add
GET /v1/user/subscriptionreturning 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 samesession.first_partyboundary as the api-key endpoints), not the retiredportalscope. - [Issue #1976] Add a per-environment Stripe billing-credentials runbook (
docs/billing/stripe-credentials.md) documenting the required env vars and thePOST /v1/billing/portalverification 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/mefor authenticated first-party account and data deletion. - Irreversible; gated on
session.first_party(PATs rejected) and requires explicit confirmation. - [Issue #1980] Add
PATCH /v1/meso an interactive first-party session can update its own name and company. - [Issue #1981] Add
PATCH /v1/user/passwordletting a portal user change their own password after verifying the current one. - Gated on an interactive first-party session (
RequireInteractiveSession); data keys (PATs, opaquetc_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-atartisan command that backfillstrial_ends_atfor 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); theplanselects the Stripe price by lookup_key (#2130); optionalsuccess_url/cancel_urlare validated against the trusted-origin allowlist. - [Issue #2035] Add a public
POST /v1/signupendpoint 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 opaquetc_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
userstable so it can be redeemed. A full founding pool returns a waitlist response and starts no trial; an expired token returns410, an invalid/already-used token returns422. - [Issue #2037] Add a Loops transactional-email driver and lifecycle notification classes (email verification, welcome, trial reminders, trial suspended).
- Requires
LOOPS_API_KEYand 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/remainingendpoint. - 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_keysschema, and coupling-seam migration decisions. - [Issue #2051] Add the
api_keystable,ApiKeymodel, and opaque-key generate/verify service as additive foundation fortc_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]toGET /v1/cards(and aGET /v1/players/{player}/cardsconvenience 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-lifecyclesends reminders or suspends trials, and always on failure; opt-in viaOPS_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 internaltrial_lifecycle_runsledger table (runphp 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/usagereturning 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 pricelookup_keyvia env (STRIPE_PRICE_STARTER_LOOKUP_KEY/STRIPE_PRICE_PRO_LOOKUP_KEY), the single identifier thatPOST /v1/billing/checkout(#2034) and the subscription webhook'sresolvePlan()agree on. Config prerequisite for self-service checkout.
Changed
- [Issue #2199] Upgrade
intervention/image3→4 (v4.2.0). Two v4 API removals affect the image-upload pipeline (ImageUploadServiceand theGenerateImageThumbnailsjob), 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 thetoJpeg()convenience wrapper). The re-encode-to-JPEG (metadata/polyglot stripping, #1488) behavior is preserved;orient,scaleDown, andwidth/heightare unchanged in v4. - [Issue #2198] Upgrade
stripe/stripe-php20→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 becauseErrorObjectstring properties were retyped tonull|stringfor stricter type-checking. No application code changes were required. - [Issue #2196] Upgrade to Laravel 13 (
laravel/framework12→13, 13.20.0), - Also upgrades the test toolchain —
phpunit/phpunit12→13 and - [Issue #2196] Upgrade to Laravel 13. Bumps
laravel/framework12→13 (13.20.0) and the first-party/related packages whose^12-only constraints would otherwise block resolution:laravel/tinker2→3,darkaonline/l5-swagger10→11, andspatie/laravel-activitylog4→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.jsonis pinned to^8.4.1to match. - Also upgrades the test toolchain —
phpunit/phpunit12→13 (13.2.4) andbrianium/paratest7.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.pendingmarker 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-hoursguard and thebilling_accountscross-check remain as defense-in-depth. - [Issue #1906] Run the project's bash test suites in CI via a new
test-build-scriptsrunner and a Lint-workflow job.build/version.shnow resolves the branch from GitHub Actions'GITHUB_HEAD_REF/GITHUB_REF_NAMEon 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
securityfield authoritative by adding a top-level bearer-auth default and marking the public/health,/version, and/webhooks/stripeoperations with an explicit emptysecurity, so consumers can classify auth requirements directly fromoperation.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_IDwithLOOPS_TRIAL_REMINDER_7DAY_TEMPLATE_ID,LOOPS_TRIAL_REMINDER_2DAY_TEMPLATE_ID, andLOOPS_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
EmailVerificationTokenprimitive (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
Deprecationresponse header on the RFC 9745 structured value@<unix-timestamp>across both emit paths by bringingAddDeprecationHeaderin line withDeprecatedRoute. - The
/internal/workflow/*legacy aliases now emitDeprecation: @<unix-timestamp>instead ofDeprecation: 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-statusscope to the least-privilegeread:draftscope now thatread:draftis 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:APIApiException/ResourceNotFounderror shape. - 404 bodies for these endpoints now carry a resource-specific
codeand the standardResourceNotFoundtitle/detail; clients that pinned the oldabort()-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
ParallelCardCopierservice 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-*.mdback to master/develop after a release build so tag-only Dependabot releases get a documented section and consumedchangelog.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
portalOAuth scope, and retire theportalscope. - Long-lived personal access tokens (data keys) can no longer reach
/v1/user/api-key; portal clients should stop requesting the removedportalscope. - [Issue #1975] Document
BILLING_RETURN_URL_ALLOWLISTandBILLING_DEFAULT_RETURN_URLin.env.examplefor the portal billing return-url flow. - Each environment must still set
BILLING_RETURN_URL_ALLOWLISTto include the portal origin out-of-band; the example value is intentionally empty. - [Issue #1977] Extend
GET /v1/meto 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 rejecttc_data keys on the account-management endpoints. - Response shape change: key metadata now reports the opaque
key_idasidand addslast_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_pooltable andbilling_accounts.has_trialedcolumn. - The dump now carries the
founding_poolid=1 sentinel seed row (a migration-inserted rowschema:dumpdoes 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 globalbilling.trial_length_daysconfig otherwise. - [Issue #2114] Widen the trial-window backfill command so a one-time run also fills
trial_started_aton already-populated trialing accounts.
Removed
- [Issue #2203] Remove the unused
js-yamldirect dependency fromdocs/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 transitivejs-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.phpscript and its invocations now that the OpenAPIinfo.versionsentinel 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
indexaction. - [Issue #2032] Correct the documented Docker Compose minimum to v2.24+, matching the long-form
env_fileobject entry indocker-compose.yml. - [Issue #2178] Guard
build/version.shagainst a repo with no reachable tags: the no-tagsgit describeno longer aborts the script underset -e, so the0.1.0-ci.Nfallback works (protects shallow CI clones that fetch history but not tags). - Wired the comprehensive
build/tests/test-version.shscenario suite into thetest-version/test-build-scriptsCI runner after making it hermetic (throwaway repos outside the tree, trap-based cleanup,masterdefault branch). - [Issue #2066] Fail closed in
FoundingPoolService::tryAcquire()when the trial-start callback links no billing account, rolling back instead of returningAcquired. - [Issue #1907] Run the merge-conflict-marker guard on every PR by moving it into a standalone
conflict-markers.yamlworkflow, so PRs touching only non-paths-listed file types no longer skip the check. - [Issue #1902] Eager-load
attributesandchildrenon the card list endpoint when honored via?includeso 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_idUUID column inUpdateVerbDeprecationTest; 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, andjersey_numberattributes 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.versionto 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
/versionendpoint andconfig('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-fullso 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.secretsloaded via the compose env_file soPOST /oauth/tokenno 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 underAPP_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:syncdrives 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, somaster/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
objectschemas now render as{}in generated examples instead ofnull, matching the schema and JSON:API expectations. - [Issue #2090] Clamp a misconfigured
billing.trial_length_daysback 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/portalon the interactive first-party session instead of thewriteOAuth scope, so a least-privilegeread:publishedportal session can open the Stripe billing portal. - [Issue #2151] Inline the request-body example as a
-dpayload 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 transitivejs-yamlcopies (→3.15.0 / →4.2.0).npm auditis 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 auditreports no PHP advisories. - [Issue #2007] Add
oauth:revoke-legacy-portal-keyscommand 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-runand--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
internalOAuth scope on per-client admin approval so only clients flaggedis_internalcan obtain or use it. - Deploy caveat: every existing
is_internal=trueclient instantly gainsinternal-scope access; audit those rows and confirm the tradingcardapi-tools daemon client is approved (cardtechie/tradingcardapi-tools#1003) before rollout. - [Issue #2112] Split the
internalOAuth scope gate onto a dedicatedcan_request_internalcolumn so scope-trust is granted independently of theis_internalrate-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 grantedcan_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:publisheddata scope instead of the managementportalscope, so a data key can no longer read or rotate its owner's key. - Security fix: pre-existing
portal-api-keyPATs still carryportaland 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 deprecatedGET /useralias, 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/mekeeps the new JSON:API resource; the/useralias 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_intgenerated sort column, which crashed production migrations on non-numeric card numbers (e.g.SD38) under strict MySQLsql_mode(aSTOREDgeneratedCAST(... AS UNSIGNED)is a write, which strict mode rejects). The set-checklist sort returns to the read-timeorderByRaw('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 tomode=minor a registry cache. - [Issue #1759] Add parallel PHPUnit execution via paratest with a
make test-paralleltarget and per-worker isolated in-memory SQLite. - PCOV now defaults to disabled in the image; coverage runs must pass
-d pcov.enabled=1to the PHP interpreter — e.g.php -d pcov.enabled=1 vendor/bin/phpunit …(themake 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()inDatasetBackupCommandto close a shell-injection vector in themysqldumpexec path. - [Issue #1465] Remove the hardcoded Mailgun API key and unused Mailgun mail config from
.docker/docker-compose.full.ymlandconfig/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_pageclamped 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 viaerrors()->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_totaltoGET /v1/workflow/actionable-setsreporting the unpaginated count of matching rows. - Additive —
meta.totalis unchanged (still the returned page size); downstream admin/SDK changes are separate follow-ups. - [Issue #1443] Expose
analyze:sales-readinessas an authenticated internal HTTP endpoint (GET /internal/analytics/sales-readiness) supportingformat=jsonandformat=table. - [Issue #1440] Add an Update Cadence section to
analyze:sales-readinessreporting 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-readinessworkflow status section. - [Issue #1493] Add a public, unauthenticated
GET /healthendpoint 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 emptyerrorsarray. meta.request_idis attached only when therequest.idbinding from #1517 is present; until #1517 lands the field is omitted.- [Issue #1495] Add
v1/player-teamsas the canonical kebab-case path for player-team associations, keepingv1/playerteamsas a deprecated alias. - Breaking-change scope: the JSON:API
typeand internal route name stayplayerteams; only the URL path changes, and the old path is preserved as an alias. - [Issue #1517] Add request-ID correlation: an
X-Request-IDresponse header on every response,request_idin log context, andmeta.request_idon 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-emptyerrors[], stringstatus,title/detail, conditionalsourcepointer/parameter, noAPP_DEBUGleakage). - [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
databasecache/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-IDcorrelation 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 injectsrequest_idinto every log record across the local, CI, and production log channels. - [Issue #1548] Centralize API error handling: add
Factory::error(), replace the reflectiveModelNotFoundExceptionhandler 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-trustedis_internalclient 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_accountstable andusers.billing_account_idlinkage with a lazyBillingAccount::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 readingstripe_customer_iddirectly. - [Issue #1567] Add an authenticated
POST /v1/billing/portalendpoint that opens a Stripe Customer Portal session for the caller and returns its URL. - New
config/billing.phpreturn_urlallowlist must be populated; an empty allowlist rejects every caller-suppliedreturn_url. - [Issue #1568] Add a signature-verified
POST /webhooks/stripeendpoint with an idempotent Stripe event log. - Requires
STRIPE_WEBHOOK_SECRETto 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:publishedend-user scope, with a feature test covering token issuance and/userlookup. - [Issue #1598] Add authenticated customer API-key endpoints:
GET /v1/user/api-keyreturns masked key metadata andPOST /v1/user/api-keyregenerates 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_atcolumn tooauth_access_tokens(run migrations). - [Issue #1611] Add unit tests for the pure helpers in the Bruno-sync scripts (
import-openapi.jsfilename/version routing andvalidate-collection.jsstructure checks), wired intodocs/scriptsnpm test. - [Issue #1615] Add a lightweight count-only
GET /internal/actionable-sets/countendpoint returning the unpaginated actionable-set count without materializing the row page. - [Issue #1661] Implement the
PlayerTeamControllerupdate and destroy actions soPATCH/PUTandDELETEon player-team associations perform work instead of no-opping. - [Issue #1670] Add a
.claude/pre-pr-gate.mddeclaring 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-limitartisan 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-alpineservice in the local Docker stack and add thepredis/predisclient so the existingrediscache/queue store is usable in development. - Local-only; production provisioning and any default-store change are tracked separately. The default
CACHE_DRIVERstaysdatabase. - [Issue #1716] Cache the paginated
GET /v1/genresandGET /v1/attributeslist 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_atonapi_cards(deleted_at)so the windowed stats deletions aggregate is index-backed on cache miss, mirroringidx_cards_created_atfor additions. - [Issue #1746] Add external error reporting via a driver-based
ErrorReporterseam (defaultsentry, DSN-gated so it is a no-op withoutSENTRY_LARAVEL_DSN; setERROR_REPORTER_DRIVER=nullto 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_idis 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 askip-changeloglabel escape hatch. - Opt out of the gate by adding the
skip-changeloglabel to the PR. - [Issue #1763] Add a periodic code-coverage workflow (
coverage.yaml) that runs on merge todevelop/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_failedwebhooks into a localinvoicestable 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.headersmiddleware 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-readinesscomparing each published base set'scard_countagainst actual cards across its subsets. - [Issue #1572] Confirm and document that
PATCH /v1/set-todos/{id}acceptsstatus=pendingfrom any state (includingreview); add regression tests and addreviewto the OpenAPI status enums. - [Issue #1441] Add an Issue Creation section to
.claude/CLAUDE.mddirecting Claude to use thecreate_cross_repo_issuesMCP tool instead of rawgh issue create. - [Issue #1456] Confirm and document that
GET /v1/workflow/actionable-sets?step=audit&status=in_progresssurfaces 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:syncidempotent so future PRs start from a cleangit status. - [Issue #1502] Move the
GET /userclosure to a dedicatedGET /v1/mecontroller and keep/useras a deprecated alias. /usernow emits aDeprecationheader pointing to/v1/me; the alias is slated for removal in v2.- [Issue #1486] Add the
https://scheme toAPP_URLin the production Docker compose file so Laravel builds correct absolute, secure URLs. - [Issue #1501] Consolidate the
v1/workflow/actionable-setsandv1/workflow/bulk-initialize[/{job_id}]endpoints onto RESTful paths (v1/actionable-sets,v1/todo-initialization-jobs) and keep the legacyv1/workflow/*URLs as deprecated aliases that emit aDeprecationheader. - 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-parallelto the JSON:API action namespacePOST 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}andGET /v1/workflow/sets/{id}/todoswith a full nestedv1/sets/{set}/todosresource (index/show/store/update/destroy). - Breaking: the old
set-todos/{id}andworkflow/sets/{id}/todospaths are removed; clients must move to the nested routes. - [Issue #1497] Express v1 genre soft-delete state via
filter[status]=deletedandinclude_trashed=truequery parameters instead of literaldeletedURL segments; keep the old routes asDeprecation-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-batchesresource; keepv1/card-images/bulkas 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
apimiddleware group sostatus.ignoreruns beforebindings, 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 asX-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
Deprecationheader 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*.devprivate keys. - Run
bash .docker/scripts/generate-dev-certs.shbeforedocker compose upafter 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 /cardsmigration example in the public API reference template with the realGET /v1/stats/{modelType}→GET /v1/stats/countsmigration, and regenerate the rendered doc. - [Issue #1475] Scope CORS away from wildcard origins to an env-driven
CORS_ALLOWED_ORIGINSallowlist with explicit paths, methods, and headers. - Deploy prerequisite: set
CORS_ALLOWED_ORIGINSin 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$fillableblocks 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,yearrelationships. - 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; nobru runbehavior 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
detailfromApiException::getErrors()DEBUG-log context so user input never reaches persistent logs. - [Issue #1527] Skip
api.loggingmiddleware'sLogApiUsagedispatch when the queue driver issync, eliminating a synchronous per-request insert intoapi_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, andparentinSetController::indexto remove the per-set N+1 thatSet::getNameAttributetriggered on the/v1/setslist 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}/checklistimports above 50 lines so large checklists no longer block the request thread;queue=true/queue=falsestill 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
queuequery param now defaults toauto; 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 addedidx_cards_created_atso 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-sourceswith a default page size of 50 and a 200 max cap to bound the previously unboundedget(). - 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 ofModel::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_intcolumn 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()savesfield_overridesvia the instancefill()->save()path so theCardarraycast applies (and a query-builderupdate()must not be substituted, as it bypasses both$castsand$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_logswrite amplification by dropping two of its four composite indexes and add a dailylogs:prune-api-usagecommand 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/snapshotsand/v1/stats/growthnow use the JSON:APIerrors[]shape (was{message,errors}); also resolves #1515. - [Issue #1551] Make transformer eager-load requirements explicit via a
DeclaresEagerLoadscontract so controllers load relations deliberately instead of relying on hiddendefaultIncludes, 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-readinessoldest-in-flight metric on the current-step todo'sstarted_at(falling back to workflowcreated_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 (
/versionand/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
ApiExceptiondiagnostic DEBUG logging out ofgetErrors()into the centralizedHandlererror path, preserving the #1524detail-key scrub. - [Issue #1710] Route the legacy
genres/deletedalias through theDeprecatedRoutemiddleware so the deprecation header rides every response the route produces, not just 2xx. - The
Deprecationheader value changes from the literaltrueto the RFC 9745@<timestamp>form, andSunset/Link/Warningheaders 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.parameterinstead ofsource.pointeron 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
databasedriver — no behavior change until an operator setsAPI_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 viaApiException/ResourceNotFound. - The 404 body now includes
code,detail, andmeta.request_id, and itstitlechanges 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 Coveragejob 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-testsCI 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 fromphpunit.xmlso 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 intoCHANGELOG.mdat release time so concurrent PRs stop conflicting on the shared## [Unreleased]section. - PRs now ship a
changelog.d/<issue>-<type>.mdfragment instead of editingCHANGELOG.md; fragments are excluded from Markdown Lint and assembled at release. - [Issue #1777] Include pending
changelog.d/fragments inmake changelog-previewso the dev-facing preview reflects unreleased changes that have not yet been collated intoCHANGELOG.md. - Read-only: the preview collates against temp copies and never mutates
CHANGELOG.mdor deletes fragments. - [Issue #1780] Commit an ADR-001 Sunset window on the deprecated
GET /useralias and document the field-leveldeprecated: trueSwagger 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-IDresponse 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.mdgate 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-driftCI backstop on in-repo release PRs (basemaster), not justdevelopPRs, so the release version the Auto Version Bump stamps intoopenapi.jsonno 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.mdand blocked PRs.CHANGELOG.mdremains 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.shnow strips a.beta-suffix to its base version on the master arm, mirroring the existing RC handling; hotfix/dependabot builds still takepatch+1once 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
ApiExceptionsubclasses so HTTP request paths return JSON:API-shaped errors instead of{"errors": []}500s. - [Issue #1504] Remove the broken
GET /v1/genres/{id}/deletedroute that referenced a non-existent controller method and returned a 500. - Also drops the now-dead
links.archivefield — pointing at that removed route — from both the trashed-genre serialization (GET /v1/genres/deleted) and theDELETE /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-leveldata.idand polymorphiconcardids resolve to{{testCardId}}/{{testSetId}}/{{testPlayerId}}), aligns update-request URL path ids with the bodydata.id, and skips the JSON body for multipart/non-JSON endpoints. - Fixed the OpenAPI request schemas for
years(usename, notyear) andattributes(add the requiredattributefield) 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 fromCheckWriteScopeandCheckDeleteScope403 responses instead of the non-conformant{error, message}shape. - [Issue #1526] Cache the
apirate 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 aforgetRateLimitCache()helper available for explicit invalidation. - [Issue #1530] Eager-load the polymorphic
onCardablerelation onCard::onCard()soOnCardTransformer'sdefaultIncludesno longer fires one lookup per on-card entity. A 25-card list with?include=oncarddrops 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/confirmonDELETE /v1/sets/{id}: refuse deleting a published set or one with remaining children with a structured 409 naming the blocker, and cascade-delete children whenforce=true, instead of returning an opaque 500 (#1579). - [Issue #1587] Use the default
GITHUB_TOKENfor the auto-version-bump PR checkout so a set-but-invalidDEPLOY_APP_TOKENno longer shadows the fallback and fails the check. - [Issue #1600] Register the advertised
read:draftOAuth 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
minimatchnpm override so it no longer capsmarkdownlint-cliat v5, fixing thelint:mdnamed-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+jsonfor all JSON:API (/v1,/v2) request and response bodies in the OpenAPI spec, matching the media type the runtime enforces, by converting the sourceOA\JsonContentannotations to explicitOA\MediaTypeentries and regeneratingpublic/docs/openapi.json. - The non-JSON:API
/healthand/versionoperations intentionally remainapplication/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-outputworkflow commands inbuild/create-host.shwith the$GITHUB_OUTPUTenvironment-file form. - [Issue #1692] Remove the deprecated
install: trueinput from the Docker Buildx setup step in all four build workflows. - [Issue #1694] Fix the doc generator so single-resource example
links.selfURLs 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:APIdetailinstead of dropping them for the canned Method Not Allowed error. - [Issue #1783] Stop the public doc generator from emitting auth headers and a bogus
/v1alias for unauthenticated infrastructure endpoints, and fix the deprecated/v2alias to point at the correct/v1path. - [Issue #1783] Stop the public doc generator from appending an example UUID to the
links.selfURL 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=oncardon the card list endpoint and batch-loadonCard.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/arrayitems.$refto the component schema's resource type, and content types in the spec match what the controllers actually return. - [Issue #1856] Pin the in-memory
arraycache 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 todatabase(#1528). - The guards assert against
DB::getQueryLog(); under the database cache store,cache()has/get/put andCache::rememberissued 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 onGET /v1/attributesnow filters results instead of throwing a 500 — thename()query scope was undefined on theAttributemodel. The scope is now shared via aFiltersByNametrait used bySet,Genre, andAttribute(replacing the per-model copies), and ignores non-scalar input such as?name[]=foorather 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_PROXIESto their LB subnet; unset falls back to RFC-1918 private ranges. - [Issue #1490] Add an explicit
$hiddenarray to the User model sopassword/remember_tokenare hidden from serialization by default (excluded unless explicitly made visible); the authenticated-user response is the narrowid/name/emailJSON:API shape served byGET /v1/me, with the deprecatedGET /useralias (#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, andqsminimum-versionpackage.jsonoverrides (^ranges, matching the existing overrides block) to clear the@usebruno/clidev-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-nginxand PHP-FPM/statusendpoints to loopback and the private subnet, keeping/pingpublic for healthchecks. - [Issue #1479] Remove the behavioral bypass of the deprecated
X-TCAPI-Ignore-Statusheader: the header no longer changes filtering, and unfiltered status access is governed by theread:all-statusscope 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
OverrideDefaultStatusmiddleware behind aread:all-statusscope check and a status allowlist before honoring theX-TCAPI-Default-Statusheader. - [Issue #1482] Reject
per_page/limitover 100 on the Card, Player, and Team list endpoints to close a pagination DoS vector. - [Issue #1483] Require the
internalOAuth scope on the audit-log and internal workflow routes. - Breaking for API clients: tokens without the
internalscope now get 403 onGET/POST /v1/audit-logs, the canonical workflow routesGET /v1/actionable-sets,POST /v1/todo-initialization-jobs, andGET /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}/workflowis unaffected. Theinternalscope 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 theapplication/vnd.api+jsonAccept 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()honorsX-Forwarded-Foronly from a trusted proxy and never spoofs the client IP from an untrusted peer or a wildcard config value. - [Issue #1557] Add an
internalOAuth 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 theinternalscope (403 without it); SDK/MCP/admin/tools callers must migrate (companion issues #214, #411, #1339, #784). - [Issue #1707] Restructure
AuthenticateWithApiOrClientCredentialsso 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_TOKENmust be present in the build environment formake up*and as theCOMPOSER_TOKENrepo secret for CI. - [Issue #1793] Confirm the residual
uuidadvisory (GHSA-w5hq-g745-h8pq) is remediated by the existingoverrides.uuid(>=11.1.1) pin and dismiss the stale Dependabot alert. - Dev-scope only; the pin and a clean lock resolving
uuidto 11.1.1 are now guarded by a regression test so the advisory cannot silently return.
Removed
- [Issue #1629] Remove the unused
OverrideDefaultStatusmiddleware and itsstatus.overridealias as dead code. - [Issue #1634] Remove the dormant
api:vintage-importcommand, its sole-consumervintagedatabase connection, and the deadVINTAGE_*environment variables; rename the misnamedtestVintageImporttest. - 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/deletedliteral-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-importconsole command (app/Console/Commands/VintageImport.php), thevintageconnection inconfig/database.php, theVINTAGE_*entries in.env.local, and the stale import command reference indocs/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.yamlstill hard-requiredVINTAGE_DB_HOST/VINTAGE_DB_USERNAME/VINTAGE_DB_PASSWORD, so production deploys aborted withMissing required deploy secret: VINTAGE_DB_HOSTonce the correspondingPROD_VINTAGE_DB_*repository secrets were retired. This mirrors the removal already present ondevelop.
📦 [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) andguzzlehttp/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 acomposer.lock-only bump with nocomposer.jsonchange.composer auditis now clean.
Fixed
- [Issue #1926] Load
chalkvia dynamicimport()in thedocs/scriptsdocumentation generator and validator. A Dependabot bump tochalk5 (ESM-only) broke the CommonJSrequire('chalk')calls, failing the "Auto Version Bump for Release PRs" workflow withERR_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 legacycirclecideploy 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/psr7to 2.11.0,phpseclib/phpseclibto 3.0.55) and bumplaravel/frameworkto 12.62.0 to close CVE-2026-48019 (CRLF injection in the default email rule). Update root npm dev tooling to clear thebrace-expansionandfast-xml-parseradvisories, and bump@usebruno/cli3.2.2 → 3.4.2 (which supersedes Dependabot #1591).composer auditis now clean. The dev-onlyuuidadvisory (GHSA-w5hq-g745-h8pq) is not resolved:uuidmoves 9.0.1 → 10.0.0, but the fix requires>= 11.1.1and@usebruno/jsstill pinsuuid <11.1.1upstream, sonpm auditreports no available fix. Tracked in #1793.
Changed
- [Issue #1788] Add a Dependabot
npmconfiguration entry for the/docs/scriptsproject 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~personalAccessTokento the barepersonalAccessTokenthat 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_KEYfrom 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.envno longer contains an encryption key, so containers fail closed (MissingAppKeyException) unless the runtime environment suppliesAPP_KEY(#1468) - [Issue #1468] Replace the shared committed
APP_KEYin the CI integration-test and local full-stack compose files with distinct, documented throwaway keys; unit tests now use a dedicated throwaway key defined inphpunit.xml(#1468)
📦 [0.8.31] - June 7, 2026
Security
- [Issue #1466] Pass the GitHub
COMPOSER_TOKENto Docker builds as a BuildKit secret mount instead of a build-arg, exposing it to composer only viaCOMPOSER_AUTHfor a singleRUN— the token can no longer appear in any image layer,auth.json, ordocker historymetadata. Applied across all four CI build workflows, the Makefile, and docker-compose builds (#1466) - Exclude
.envandauth.jsonfrom the Docker build context via.dockerignoreso local secrets can never enter an image layer throughCOPY . .(#1466) - The previously-leaked
COMPOSER_TOKENPAT 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-devheader 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 theteststage;gnupgis installed and purged within the nginx-install layer inbase(#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
Dockerfileinto 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), npmnode_modules, the Node.js runtime, or the Bruno CLI — reducing image size by several hundred MB. Local dev and CI test jobs use thetesttarget, which preserves previous behavior (#1667) - Add a CI guard step to
build-and-test.yamlthat 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_TOKENinto images:composer config github-oauthpreviously 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 achmod 600.envfile on the host bydeploy.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-bumpworkflow for Dependabot PRs to prevent cross-PR conflicts onCHANGELOG.mdandpublic/docs/openapi.jsonwhen other PRs merge to master. Version-bump still runs post-merge viabuild-release.yaml/build-develop.yamlso the version history is unaffected (#1452)
📦 [0.8.24] - April 19, 2026
Security
- Bump
laravel/passportto^13.7.1addressing TokenGuard authenticating unrelated user for client-credential tokens (GHSA #138, high) (#1448) - Bump
phpunit/phpunitto^12.5.22addressing argument injection via newline in INI values (GHSA #143, high) (#1448) - Bump transitive
protobufjsto 7.5.5 via npm override addressing arbitrary code execution (GHSA #141, critical) (#1448) - Bump transitive
axiosto 1.15.0+ via npm override addressing NO_PROXY SSRF and cloud-metadata exfiltration (GHSA #140, #142) (#1448) - Bump transitive
follow-redirectsto 1.16.0 via npm override addressing cross-domain auth header leak (GHSA #139) (#1448) - Bump transitive
phpseclib/phpseclibto 3.0.51 addressing variable-time HMAC comparison in SSH2 (GHSA #135, low) (#1448)
Changed
- Bumped GitHub Actions (SHA-pinned):
appleboy/ssh-action1.2.2→1.2.5,chrnorm/deployment-action2.0.5→2.0.8,actions/github-scriptv8→v9 (#1448) - Grouped
github-actionsdependabot updates into a single PR to reduce merge churn and keep SHA pinning consistent (#1448) - Bumped
dotenvdev dependency to^17.4.2(#1448)
📦 [0.8.23] - April 19, 2026
Added
todo_idandnotesfields onGET /v1/workflow/actionable-setsresponse items so the admin review-queue UI can render reasons and wire Resolve/Skip actions without a second lookup (#1445)notesfield on workflow todo responses (GET /v1/sets/{id}/workflowandGET /v1/workflow/sets/{id}/todos) (#1445)
📦 [0.8.22] - April 16, 2026
Added
- Added
analyze:sales-readinessartisan 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=Nflag - 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=jsonoutput
📦 [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_logstable 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
auditenrichment step to set workflow that auto-creates whenpublishcompletes (#1375) - Added
step=auditfilter toGET /v1/workflow/actionable-setsendpoint 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
reviewstatus toset_todosfor flagging steps that need human intervention (#1395) - Added
statusquery parameter toGET /v1/workflow/actionable-setsendpoint to filter by todo status (e.g.status=review)
📦 [0.8.16] - April 9, 2026
Fixed
- Fixed deployment status stuck in
in_progresswhendocker-tag-latestjob fails by adding a dedicated status-finalizer job (#1407)
📦 [0.8.15] - April 4, 2026
Removed
- Removed unused
markdown-tocdependency and 59 transitive packages (including deprecatedlodash.template) fromdocs/scripts(#1405)
📦 [0.8.14] - April 4, 2026
Fixed
- Pin third-party GitHub Actions to specific versions to resolve deprecated
set-outputwarnings (#1404)
📦 [0.8.13] - April 4, 2026
Changed
- Replace deprecated
avakar/create-deployment@v1andavakar/set-deployment-status@v1withchrnorm/deployment-action@v2andchrnorm/deployment-status@v2for 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=backlogandpriority=allon validate step (#1374)
📦 [0.8.10] - April 4, 2026
Added
workflow:bulk-validatecommand 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-satisfactioncommand 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-initializeendpoint now returns diagnostic metadata (resolved_count,meta) so callers can understand why 0 sets were initialized (#1371)bulk-initializeparent-source lookup now excludes soft-deleted parent sets, consistent withSetObserverbehavior (#1384)
📦 [0.8.6] - March 29, 2026
Fixed
bulk-initializeendpoint 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 skipdiscover_sourcesstep, regardless of parent source state (#1387) cleanup:subset-source-satisfactioncommand now also fixes parallel sets with pendingdiscover_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-satisfactionartisan 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 indexapi_cards(set_id, deleted_at)to
📦 [0.8.2] - March 22, 2026
Fixed
GET /v1/workflow/actionable-sets?step=validate504 gateway timeout — added composite indexes onapi_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}/todosendpoint returning the ordered todo list for a set's workflow (#1367)
📦 [0.8.0] - March 21, 2026
Added
set_workflows,set_todos, andset_todo_historytables for API-side workflow state tracking (#1310)WorkflowStepandTodoStatusPHP enumsSetWorkflow,SetTodo, andSetTodoHistoryEloquent models with relationshipscreatingobservers onSetTodoandSetTodoHistoryto auto-deriveset_idfrom parent record, preventing denormalized field mismatchesset_idindex added toapi_set_todo_historymigrationPATCH /v1/set-todos/{id}endpoint to update workflow todo status (#1311)- Step dependency enforcement: returns 422
DEPENDENCY_NOT_METwhen 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_workflowandset_todoson 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_sourcessatisfaction from their parent DiscoverSourcesstep added to workflow for parallel, insert, autograph, and relic sets (#1348)GET /v1/workflow/actionable-setsendpoint — 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-initializeendpoint — 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}/workflowendpoint — exposes full set workflow state and todos (#1340)- Enrichment step architecture (#1349)
isEnrichment()method onWorkflowStepenum classifies orthogonal steps that do not depend on prior workflow stepsGET /v1/workflow/actionable-setsandPATCH /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, andgenerateExampleFromSchemahelper functions ingenerate-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+jsonheader (#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
SetObserverfor 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_idattribute 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 onGET /v1/cardsendpoint for filtering cards by set
📦 [0.7.66] - February 23, 2026
Added
- OpenAPI annotation for
DELETE /v1/sets/{id}/checklistendpoint - Input validation for
from_numberandto_numberquery 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.phpURLs (#1289)
📦 [0.7.63] - February 22, 2026
Added
DELETE /v1/sets/{id}/checklistendpoint for bulk card deletion with?queue=trueasync 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 = nullor0now get the default authenticated limit instead ofLimit::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 noparent_idis 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_countfrom parent whencard_countis 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_idandsource_typequery 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:
/Xfor numbered sets (e.g., "Black Foil /10") - Shows
1/1for superfractors (serial = 1) - Skips append if name already ends with a serial suffix to prevent duplicates
- Run
php artisan api:update-names --setsafter 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_oncardto 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_oncardto 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_ordercolumn toapi_on_cardtable 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
CreateCardForParallelSetandUpdateCardForParallelSetjobs to preserve sort_order - Added
sort_orderto API response viaOnCardTransformer
📦 [0.7.39] - December 26, 2025
Added
- API endpoint test coverage for parallel set
card_countinheritance (#1205) - Verifies
card_countis inherited from parent when creating parallel sets via API - Confirms explicit
card_countvalues 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_countfrom parent - Explicit zero
card_countpreservation - Parallel set without
parent_id(no inheritance) - Taxonomy inheritance with explicit
card_count - Very large
card_countvalues (9999+) - Sets with both
is_parallelandis_insertflags - Inheritance of null
card_countfrom 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-parallelendpoint 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=trueparameter - Addresses limitation where
ParallelizeSetonly runs on initial parallel set creation
📦 [0.7.35] - December 25, 2025
Added
- Explicit tests for
current_card_countfield accuracy after card changes (#1157) - Verifies cache invalidation correctly updates
current_card_countin 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 /10now extracts serial as10instead of including/10in team name - Supports both
/10and#/10formats at end of line - Maintains backward compatibility with existing
#/ 10space-separated format
📦 [0.7.33] - December 23, 2025
Fixed
parent_idfilter 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/setsendpoint viayearquery parameter (#1191) - Filter sets by year ID (UUID) for targeting specific release years
- Enables
/prepare-publicationworkflow to focus on recent releases
📦 [0.7.31] - December 23, 2025
Added
subset_countfield 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
withCountto prevent N+1 queries
Fixed
- Optimized
current_card_countfield in SetTransformer to use eager loading (#1192) - Also uses
withCount('cards')to prevent N+1 queries - Both
subset_countandcurrent_card_countnow 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=truedispatches 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_overridesJSON 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_paralleltoggle not dispatching ParallelizeSet job via API - Enables MCP
parallelize_settool 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-splitscommand 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 NametoPlayer Name - Team Nameformat 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
breaktocontinueto 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_variationboolean 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-snapshotsto 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_lockstable 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-snapshotsArtisan command for capturing entity count snapshots- Scheduled to run daily at 04:00
--dateoption 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,teamsupport - Enables MCP tool
get_playerteamto 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
includequery 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
imagesrelationship to Card schema - Completes documentation for Card Images feature (v0.7.0)
- Defined as one-to-many array of card_images resources
- Added
sourcesrelationship 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-imageswith 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
?sizeparameter - Graceful failure handling with 3 retry attempts
php artisan images:regenerate-thumbnailscommand 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=imagessupport in Card API responses
Configuration
- New
config/images.phpfor centralized image configuration .env.exampleincludes comprehensive image/CDN variablesIMAGE_STORAGE_DISKenv 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_uuidfield inapi_cardstable 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_sourcestable 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
sourcesinclude 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/cardsand/v2/setsendpoints - 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
/issuecommand - 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
commitparameter 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/setsendpoint pagination to acceptper_pageparameter (#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_teamstable (#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
createandeditroutes (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/setsendpoint pagination (#997)- Accept both
per_page(JSON:API standard) andlimit(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)
ApiUsageLoggingmiddleware for automatic request trackingLogApiUsagejob for asynchronous database logging- Database migration for
api_usage_logstable 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-secretsartisan 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-versionunified test runner for version script validation- Improved npm test scripts:
npm testnow 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.shwhere 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\V1namespace - OpenAPI documentation includes both legacy and V1 versioned endpoints with separate tags
make changelog-updatecommand 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-previewcommand 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-updatecommand to automatically organize changelog with next version - Added
make changelog-previewcommand to see next version and current unreleased changes - Added changelog requirements to pull request template
- Added
--masterflag 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 managementGET|POST /sets- Set management with checklist supportGET|POST /players- Player managementGET|POST /teams- Team managementGET|POST /playerteams- Player-team relationship managementGET|POST /attributes- Card attribute managementGET|POST /object-attributes- Object attribute relationshipsGET|POST /genres- Genre management with soft delete supportGET|POST /years- Year managementGET|POST /manufacturers- Manufacturer managementGET|POST /brands- Brand managementGET /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
- API Endpoints - Complete endpoint reference
- Data Models - Response structure documentation
- Rate Limits - Usage limits and best practices
- Authentication - Setup and configuration
🆘 Help & Feedback
- Support - Guides, FAQs, and how to get help
- Report an API Issue - Tell us about a bug
- All Releases - Complete release history on this page
🛠️ Development Tools
- API Explorer - Interactive API testing
- SDKs - Official client libraries
- OpenAPI Specification - Technical API specification
🏷️ 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
- Email Newsletter - Monthly API updates
- Developer Blog - Technical articles and release announcements
💬 Support
- Email Support - Ask questions about API functionality
- Support Center - Guides, FAQs, and troubleshooting
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