Repository navigation
release: v1.6.69 - #294
Merged
Merged
release: v1.6.69#294
Conversation
- Drop the 'public' visibility from Utils::urlToStorefrontFile: a bucket with BucketOwnerEnforced rejects any PUT carrying an ACL, so put() returned false. - Add File::signStoredUrl()/s3KeyFromUrl(): turn absolute URLs stored as strings (legacy unsigned bucket URLs, expired signed URLs) back into a key and re-sign. - Re-sign template builder image src at render time. - Cache signed URLs for 60 of their 120 minutes so every URL handed out has at least an hour left; cut Extension icon_url cache from 24h to 30m.
Replace the db:backup command, which piped mysqldump through gzip without pipefail, swallowed upload errors and returned success on a failed dump. That is how production uploaded 20-byte empty dumps on 2026-09-24/25 (no mysqldump in the image) and then nothing at all while reporting DONE. - DatabaseBackupService streams the dump client's stdout into gzip in PHP (no shell pipeline), passes the password via MYSQL_PWD, and fails a run on a non-zero exit, a missing '-- Dump completed' marker, a dump under min_size_bytes, or an uploaded object whose size differs from the file. - Uploads go to any filesystem disk (bucket override for s3, key prefix); retention by age and/or count runs only after a fully successful run and always keeps each database's newest backup. - Every attempt is recorded in database_backups (status, size, duration, error, trigger); failures can email configured addresses. - Settings live in system.database-backups (env defaults in config/database-backups.php) and drive the schedule; disabled by default. - Admin endpoints under int/v1/database-backups: settings get/save/reset, recent runs, and a queued 'run now'. - db:backup exits non-zero on any failure; --force runs while disabled.
Add VerificationCode::issue(), check() and attemptsLeft() for flows where a leaked table must not give away live codes: - issue() stores an HMAC of the code, keyed by the app key, and hands the plain code back once on the instance (plainCode), defaulting to a 10-minute expiry and an 'active' status. - check() compares with hash_equals, counts wrong attempts in meta and locks the code on the last allowed one. Expired and locked codes report as such. - The creating hook keeps a code that was already set, so issue() is not overwritten; codes made the old way still get a random one and still check. Existing generators and their callers are unchanged. First user: the FleetOps public tracking page's one-time codes.
Adds the realtime channel authentication core, switched on by SOCKETCLUSTER_AUTH_KEY (config auth_key, publish_url, token_ttl): - SocketToken mints and verifies HS256 socket tokens (iss/aud/iat/nbf/exp/jti plus kind, sub, cid, cpid, env, ids, adm, scp, sid); anything not HS256 with the configured key, or with a wrong issuer/audience or out-of-range time claims, is rejected. Includes the scoped public tracking token. - SocketSignature derives per-purpose HMAC keys and signs/verifies the timestamped requests exchanged with the socket server. - SocketPrincipal, ChannelDecision, the SocketChannelResolver contract and the SocketChannelRegistry extensions register their channel prefixes with. - ChannelAuthorizer applies install, expiry, scope, system and self rules, then the prefix resolver, caching decisions per token and channel. - Core resolvers for company, api, user, test, install/uninstall, chat, chat_channel, chat_participant, chat_message and file channels. - The registry and authorizer are container singletons.
- POST int/v1/socket/token (console session): a user token for the current
company, in the sandbox environment when the console is in sandbox mode.
- POST v1/socket/token (public API): an api token for an API credential; for a
Sanctum user token, the principal a registered resolver claims, else a user
token.
- POST int/v1/socket/authorize: called by the socket server only, admitted by
its signature (VerifySocketSignature) rather than a session; re-verifies the
token and returns {allow, ttl, reason}. Exempt from the configured-instance
check so an install page can follow the install channel before setup ends.
- Every mint route answers 404 while SOCKETCLUSTER_AUTH_KEY is unset.
When SOCKETCLUSTER_AUTH_KEY is set, the broadcaster and
SocketClusterService::publish()/send() post every channel of a broadcast in a
single request to {SOCKETCLUSTER_PUBLISH_URL}/publish, signed with the derived
publish key and short timeouts. Without the key the websocket publisher is
used as before.
Channels ending in "." (an empty suffix, e.g. a session read in a queue
worker) and names the socket server would reject are dropped before
publishing.
… channel
The SocketCluster settings test ignored nothing: any admin could publish an
arbitrary payload to any channel. It now always publishes to
test.{current user uuid} and returns that channel so the console can
subscribe to it.
POST v1/socket/system-token in the fleetbase.platform-api group mints a system token. A separate path because the platform and public API groups share the v1 prefix, where v1/socket/token is the public API mint route.
…annel decision The install-channel test skipped the users table but still seeded it. The cross-company and driver tests now compare every channel's decision reason (and any resolver error logged) in one assertion.
… model ChatMessage always eager loads its attachments, so authorizing a chat_message channel queried chat_attachments for nothing (and failed where that table is absent). Channel lookups now load only the model's own row.
getCountryCodeByCurrency() and getCountryCodeByName() rebuilt the full countries dataset (a 4.7 MB JSON file plus flag hydration) on every call. Storefront serializes a country per store, so listing stores paid that cost once per record and network store lists could take minutes. The name/ISO2/currency rows are now built once, kept in the application cache and memoized per process. Cache failures fall back to building the lookup, and flushCountryLookup() resets it.
Store media, product images and proofs of delivery are looked up by subject_uuid, which had no index, so each lookup scanned the whole files table.
fix(files): support a fully private S3 media bucket
feat(backups): settings-driven database backups that fail loudly
feat(verification): hashed one-time codes with attempt counting
perf: cache the country lookup and index files.subject_uuid
Socket authentication was on as soon as SOCKETCLUSTER_AUTH_KEY was set. That
also moved every broadcast to the socket server's HTTP publish endpoint, so
provisioning the key before every client fetched tokens (or before the new
socket server was deployed) would break existing socket clients.
- New `broadcasting.connections.socketcluster.auth_enabled`
(SOCKETCLUSTER_AUTH_ENABLED, default false). SocketToken::enabled() now
needs the switch and a valid key. Every gated path follows: token routes,
the authorize endpoint and its signature check, and HTTP publishing.
- With the switch off, the console's socket test publishes to the requested
channel (default `test`) as before. With it on, only to `test.{user}`.
- README: the switch and the rollout order (ship clients that fall back on
404, switch on with the socket server in log mode, then enforce).
RunDatabaseBackup::dispatch() comes from the Dispatchable trait. Several test files define an empty Illuminate\Foundation\Bus\Dispatchable shim, so when one of them loads first the static dispatch() is missing and DatabaseBackupsTest fails depending on test order (PHP CI on release/v1.6.69 fails this way). Dispatching through the bus contract behaves the same at runtime and does not depend on the trait.
fix(backups): queue the manual backup through the bus dispatcher
…er handshake The PHP publisher connected without an Origin header, which socketcluster-server treats as '*'. With origins restricted (scripts/docker-install.sh restricts them to the console host), every server broadcast was refused with 'Invalid origin: *'. SOCKETCLUSTER_ORIGIN now sets the header through phrity/websocket's headers option. Reported in #290.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #294 +/- ##
=============================================
Coverage 100.00% 100.00%
- Complexity 7931 8880 +949
=============================================
Files 438 472 +34
Lines 25665 27918 +2253
=============================================
+ Hits 25665 27918 +2253
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
feat(socket-auth): authenticated realtime channels (tokens, authorizer, signed publish)
DB_BACKUP_BUCKET now has no default, so an install writes to the bucket it configures rather than a hard-coded name.
- deleteRecord passes the bare column name to isColumn(), matching the other record lookups in HasApiModelBehavior - add ReportFilter so report listings follow the same company constraint as other company-owned resources - tests for both
…ttings resolveNotifiable() read the definition from the company's notification_settings, which saveSettings stores as given. dynamic:<property> reached any public no-argument method on the subject, through resolveDynamicNotifiable() or the Eloquent relation getter, so dynamic:dispatch ran Order::dispatch() whenever a notification fired; a class definition was built with app(). Definitions not registered with registerNotifiable() are now skipped.
The coverage gate needs 100% per file. A typed relation that throws on a blank model is now tested (it is dropped), as is mutateModelWithRequest() on a non-model. The Expandable branch loses a Closure check that could never fail: isExpansion() only holds for a registered Closure.
…orefront-api-requests
getSessionScope returns no scope when the request has no session. Every test request had one, so the release branch fell one statement short of the coverage floor after #300.
getCompanyUuid called $request->session() unguarded, so generateQueryCacheKey threw "Session store not set on request." before getSessionScope's hasSession check could apply. Check hasSession first and fall back to the user and request input as before. The test now covers the sessionless path through generateQueryCacheKey instead of calling getSessionScope by reflection.
feat(api-cache): writes invalidate every cache that embeds the written table
…orefront-api-requests
…let extensions relate ids
Core no longer names storefront. Every route that runs LogApiRequests opted in
through its middleware group (core's public API or an extension's), so
shouldLog only skips internal routes and requests without a route, instead of
requiring the core v1/ prefix. isPublicRequest is back to v1/ only, so
storefront filters and resources are unchanged.
LogApiRequest::RELATED_ATTRIBUTE ("api_request_log_related") is a request
attribute whose ids are added to the log's related column. Extensions set it
from their middleware, for example storefront with the public id of the store
or network whose key made the request.
… methods and an unresolvable transactions manager #301 left three statements in ApiModelCache unrun, so the release branch is under the 100% coverage floor.
fix(api-logs): log every route that opts in to LogApiRequests and let extensions relate ids
fix(api): only real relations can be requested through with, expand and counts
…lowlist fix(notifications): resolve only registered notifiables from saved notification settings
…and a circuit breaker Every SMS now passes Fleetbase\Support\Sms\SmsGuard before a provider is called, and for verification codes before the code row is written. The guard validates the destination with libphonenumber (valid, mobile, not a premium or satellite range), enforces a per-destination cooldown and caps, a per-IP layer, per-tenant and per-company budgets and a platform-wide bucket, then counts the send. A company that uses up its daily budget, hits its hourly budget twice, or texts a burst of distinct new numbers is paused automatically and the system admins are alerted (mail plus in-app). Trusted purposes (2FA, admin actions) skip tenant pauses and budgets so staff can still sign in. Limits, the enforce/monitor mode, pauses and alert recipients are a stored system setting (config/sms.php supplies defaults), changeable at runtime with the sms:mode, sms:pause and sms:unpause commands or a later admin page. A refused send throws SmsBlockedException with its own status (422 bad number, 429 limited with Retry-After, 423 paused); existing email fallbacks keep working. Allowed sends record who asked and from where on the code row (meta.trace) and in sms.sent / sms.blocked log entries. The onboarding resend route gets a per-address throttle in front of the guard. Test bootstraps keep the guard off unless a test turns it on.
…annels
addRelationshipSpecificChannels checked isset($model->driver_assigned_uuid) but then built the
channel from $model->driverAssigned_uuid, an attribute that never exists, so every order lifecycle
event broadcast on a dead 'driverAssigned.' channel instead of 'driverAssigned.{driver uuid}'.
Read the same snake case key it checks.
…st comment.created
Comments carry meta.visibility = internal | customer (a 'visibility' shorthand is accepted on
create). When none is given: replies follow their parent, comments on orders are internal when
written through an internal (console) route and customer from anywhere else, and comments on other
subjects keep no visibility, which stays visible, so existing data and callers are unaffected.
- Comment::scopeVisibleToCustomers() (everything but internal) and scopeInternal(),
getVisibility(), isInternal(), isVisibleToCustomers()
- the public v1 comments API excludes internal comments from the listing (CommentFilter
queryForPublic) and find, accepts an optional visibility on create, and the Comment resource
hides internal replies outside internal requests and exposes a top level 'visibility'
- new CommentCreated event broadcasts 'comment.created' with the comment resource on
company.{company_uuid} for every comment and, for customer visible comments only, on the
subject's channels ({subject type}.{public_id} and {subject type}.{uuid}, e.g. order.order_x);
a broadcast failure is logged and never fails the comment
Adds a nullable json meta column to chat_participants and chat_messages (guarded migration). - ChatParticipant: meta (fillable, json cast), ROLE_* constants (customer, driver, store, system) and getRole() reading meta.role; the resource adds 'role' and 'meta', so a message's sender.role reaches the apps over HTTP and the chat_message broadcasts - ChatMessage: meta (fillable, json cast), isSystem() (meta.system) and getSystemKind() (meta.kind); the resource adds 'system' and 'meta' and renders a missing sender as null instead of failing (the duplicate public 'sender' key it dropped was always overwritten by the participant resource) The public send-message endpoint still builds its own attributes, so customers cannot post system messages or set roles; existing chat behaviour is unchanged.
…ainer The blocked-prefix and satellite fixtures no longer matched the numbers they were meant to refuse, a bypassed reservation reported a trace, and the policy test's users fixture mixed column sets. Eloquent queries on a container with no connection resolver recurse until memory runs out, so the policy's company and admin lookups now skip when there is no database (only unit tests).
…nd the admin API Phase 1 of the SMS guard (phase 0 is the guard itself): - sms_sends ledger: every allowed send is reserved and settled with the provider outcome, every refusal is written with its reason, with tenant, purpose, source, address, destination country and the code row it carried. sms:ledger:prune masks numbers after 30 days and deletes rows after 90 (both configurable). - verification_codes gain company_uuid, identity, channel, ip_address, user_agent, sms_send_uuid and consumed_at (identity backfilled from meta for recent rows). generateSmsVerificationFor(['hashed' => true]) stores an HMAC and hands the message callback the plain code; findActiveFor()/findActiveForSubject() find the newest live code (new column or legacy meta), check() verifies, consume() uses it up. Everything degrades to meta-only on a table without the columns. - Two breaker signals read from the data: a burst to a country the company has never texted (armed after 50 ledgered sends), and SMS codes issued with almost none verified in the last hour. - SmsServiceChannel: a notification channel that sends through SmsService, so notification texts are guarded and ledgered too. - Admin API under int/v1/sms-guard: settings (mode, budgets, blocklists, alert recipients, per-company exceptions), reset, pauses, usage per organization and the ledger with filters. The console page follows separately. - The unrouted legacy Twilio-direct SMS login methods are removed.
feat(sms): guard every SMS send with destination validation, budgets and a circuit breaker
…-visibility Fix driver uuid broadcast channel; comment visibility and comment.created; chat participant roles and system message meta
feat(sms): ledger, hashed single-use codes, learned breaker signals and the admin API
…eft behind #307 removed the unrouted legacy SMS login methods from AuthController along with their tests in AuthControllerVerificationTest, but the same cases also lived in AuthControllerLoginBootstrapTest. Those five now fail PHP CI on release/v1.6.69 with BadMethodCallException.
The clover artifact is what shows which lines are short, and it was only uploaded after a passing gate.
…gate flagged The release branch's coverage gate was never evaluated while the orphaned authenticateSmsCode tests failed first. With those gone it reports 73 statements short across the SMS guard, the comment broadcast, the exception handler and the notification registry. This covers them: - breaker: expired pauses, a second hour at budget, the platform threshold, two signals tripping on one send, sends with no company, a missing platform counter, scope lookups and conversion reads when a table is gone, and alert delivery to administrators and plain addresses, including a failed delivery - guard: a pause recorded in monitor mode, the configured cache store and its fallback; alert: every signal's wording, via() and toMail() - context: a network tenant, an unresolvable request, an unknown path - ledger and policy: a cache that is itself unreachable, tables that cannot be queried, purposes without a name or rules; validator: a number the parser rejects outright - comment broadcasts: a bare, preloaded, unknown or broken subject - handler: a refused SMS keeps its status and Retry-After (the test response factory now passes headers through, as the real one does) - verification codes: legacy rows on a table without the tracing columns and a code issued with no request to read the client address from - notification registry: an unknown definition and a registered notifiable that is not a model Two catch blocks the harness cannot reach (app() and session() never fail in it) are marked for coverage with the reason.
docs(release): v1.6.69 notes cover everything merged since #291
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release v1.6.69
Release branch for core-api v1.6.69. Merging into
maintriggersrelease.yml, which validates thatcomposer.jsonand the first line ofRELEASE.mdname1.6.69, then pushes thev1.6.69tag.This branch collects:
fleetbase/core-api ^1.6.69.files.subject_uuidindex. Adds a migration.Before merging
database_backups(feat(backups): settings-driven database backups that fail loudly #288) and thefiles.subject_uuidindex (perf: cache the country lookup and index files.subject_uuid #291).SOCKETCLUSTER_AUTH_ENABLED(defaultfalse),SOCKETCLUSTER_AUTH_KEY,SOCKETCLUSTER_PUBLISH_URL,SOCKETCLUSTER_TOKEN_TTL. Socket auth stays off untilSOCKETCLUSTER_AUTH_ENABLED=true, even with a key set, so existing socket clients keep working.