Skip to content

v1.6.69

Choose a tag to compare

@github-actions github-actions released this 14 Oct 04:50
· 53 commits to main since this release
f7bb580

v1.6.69 — SMS send guard, authenticated realtime channels, API cache invalidation, comment visibility and database backups

Added

  • SMS send guard. Every SMS passes Fleetbase\Support\Sms\SmsGuard before a provider is called, and before a verification code row is written: destination validation with libphonenumber (mobile numbers only; premium-rate, shared-cost, satellite and blocked ranges are refused), a per-destination cooldown and caps, a per-IP layer, store/network and company budgets (200 a day per company by default, 20 for companies younger than 7 days), a platform-wide bucket, and a circuit breaker that pauses a tenant and alerts the system administrators when it uses up its daily budget, hits its hourly budget twice in a day, texts a burst of distinct new numbers, texts a country it has never texted, or issues codes that nobody verifies. Trusted purposes (2FA, admin sends) skip tenant pauses and budgets so staff can still sign in. Refusals are SmsBlockedException, rendered in the usual error envelope with 422 (number can never be texted), 429 with Retry-After (cooldown or budget), 423 (paused) or 503 (counters unreachable). Limits, mode, pauses and alert recipients live in the system.sms-guard setting; config/sms.php supplies the defaults, and sms:mode, sms:pause and sms:unpause change them without a deployment. (#305, #307)
  • SMS send ledger. Every allowed send is reserved in sms_sends and settled with the provider outcome, and every refusal is written with its reason, with tenant, purpose, source, client address, destination country and type. sms:ledger:prune masks numbers after 30 days and deletes rows after 90. Writes are best effort: a missing table never fails a send. (#307)
  • SMS guard admin API under int/v1/sms-guard: settings (mode, budgets, blocklists, alert recipients, per-company exceptions), reset, pauses (create and lift), usage per organization against its budget, and the ledger with filters. The console page ships in fleetbase/fleetbase v0.7.69. (#307)
  • SmsServiceChannel, a notification channel that delivers through SmsService, so notification texts are guarded and ledgered too. (#307)
  • Verification code tracing and single-use hashed codes. verification_codes gains company_uuid, identity, channel, ip_address, user_agent, sms_send_uuid and consumed_at. generateSmsVerificationFor(..., ['hashed' => true]) stores an HMAC and hands the message callback the plain code; findActiveFor() and findActiveForSubject() return the newest live code, check() verifies it and consume() uses it up. VerificationCode::issue() stores an HMAC and returns the plain code once, and check() counts attempts and reports valid, invalid, expired or locked. The existing generators are unchanged. (#289, #307)
  • Authenticated realtime channels. Socket tokens, a channel authorizer with per-resource resolvers, and signed HTTP publish. POST int/v1/socket/token mints a user token, POST v1/socket/system-token serves platform API callers, and POST int/v1/socket/authorize is called only by the socket server, with signed requests. Extensions register channel resolvers for their own resources. It is off by default and stays off until SOCKETCLUSTER_AUTH_ENABLED=true, even with a key set, so existing socket clients (mobile apps, the console, integrations) keep working. The publisher now sends a configurable Origin (SOCKETCLUSTER_ORIGIN) on its handshake, so a socket server with restricted origins accepts server broadcasts. (#290)
  • Comment visibility and comment.created. Comments carry meta.visibility (internal or customer), accepted as visibility on create. A reply takes its parent's visibility; a comment on an order is internal from the console and customer from anywhere else; comments on other subjects keep no visibility and stay visible. The public v1 comments API excludes internal comments, and the comment resource gains a top-level visibility. A new comment.created broadcast goes to company.{uuid} for every comment, and customer-visible comments also go to the subject's own channels (order.order_x). (#306)
  • Chat participant roles and system messages. chat_participants and chat_messages gain a meta column; participants expose role (customer, driver, store, system) and messages expose system and meta (for example kind: order_updated). (#306)
  • API cache dependencies. Cache keys carry the versions of every table they embed, so a write to a related table invalidates the lists, records and relationships that embed it. Dependencies come from protected $apiCacheDependencies on the model, the request's with, expand and counts, and the lookups' own paths. withoutInvalidating() scopes coalesced high-frequency writes, and API_CACHE_STRICT_DEPENDENCIES makes a bad declaration throw in test suites. (#301)
  • Database backups. Settings-driven backups (db:backup) on a configurable schedule, with environment defaults in config/database-backups.php (DB_BACKUP_*) and an admin API under int/v1/database-backups. Failures email the configured addresses and are logged. (#288, #295, #296)
  • LogApiRequest::RELATED_ATTRIBUTE (api_request_log_related): an extension sets it on the request and the ids land in the log row's related column, so a storefront request traces back to its store or network key. (#302)

Changed

  • API cache invalidation. Invalidation now changes the keys of every cache: lists moved on the first write only after a second write, and single records and relationships relied on a tag flush that leaves values behind on phpredis 6.1 and later. Versioned keys are the invalidation mechanism, list keys cover the request path and the session scope (api.cache.scope.session), writes inside a transaction bump again on commit, and an invalidation failure never fails the write that triggered it. On deploy every key changes once. (#299, #300, #301)
  • API request logs. Any route that attaches LogApiRequests is logged, not only routes under the core v1/ prefix, so storefront API writes are traceable. Internal routes are still skipped. (#302)
  • without[] uses makeHidden(), so it no longer un-hides columns the model hides. Requested relation names come back as declared, so subfleets loads as subFleets and renders. withCounts() accepts an array. (#303)
  • Private media buckets. Stored file URLs that point into the configured s3 bucket are signed again on read, so the bucket can be fully private. (#287)
  • Faster lookups. The country lookup is cached, and files.subject_uuid is indexed. (#291)
  • The admin SocketCluster test always publishes to test.{current user uuid} and returns the channel it used. (#290)
  • The manual database backup is dispatched through the bus, and DB_BACKUP_BUCKET no longer has a hard-coded default: with no bucket set, the s3 disk's own bucket is used. (#295, #296)

Security

  • Requested relations must be relations. with, expand, with_count and count reached Eloquent unchecked, so a request could run any public method with no required arguments on a model (with[]=dispatch dispatched a blank order). A name is now accepted only when the method declares an Eloquent Relation return type (plain or union), is an Expandable closure that does, or is listed in the model's protected $withable. Nested paths are checked segment by segment and cut at the first that fails; rejected names are dropped without an error. (#303)
  • Only registered notifiables are resolved from saved notification settings. A stored dynamic: name or class that no extension registered through NotificationRegistry::registerNotifiable() is skipped instead of being called or built. (#304)
  • Deletes are scoped to the company. deleteRecord compared a qualified column name against the table's columns and never matched, so the company constraint was skipped. Reports are filtered by company through the new ReportFilter.
  • A failed comment publish no longer leaves every model unguarded in a long-lived Octane worker. (#298)
  • Order lifecycle events reach driverAssigned.{driver uuid} again; they went to a dead driverAssigned. channel. (#306)

Removed

  • MysqlS3Backup, S3BackupTrimmer and config/laravel-mysql-s3-backup.php, replaced by the new database backups. (#288)
  • The unrouted legacy Twilio-direct SMS login methods on AuthController. (#307)

Upgrade Steps

  • Run migrations: database_backups (#288), the files.subject_uuid index (#291), meta on chat_participants and chat_messages (#306), sms_sends and the verification_codes tracing columns (#307). Existing comments and codes are untouched.
  • The SMS guard is on (SMS_GUARD_ENABLED, default true) in enforce mode (SMS_GUARD_MODE). To watch before refusing, deploy with SMS_GUARD_MODE=monitor or run php artisan sms:mode monitor, review the sms.would_block log entries, then php artisan sms:mode enforce. The counters must live in a store shared by every worker (Redis in production); SMS_GUARD_CACHE_STORE pins one. Set SMS_GUARD_ALERT_EMAILS for alerts beyond the system administrators, and SMS_GUARD_BLOCKED_COUNTRIES / SMS_GUARD_BLOCKED_PREFIXES as needed. Schedule php artisan sms:ledger:prune daily (SMS_GUARD_LEDGER_RETENTION_DAYS, SMS_GUARD_LEDGER_MASK_AFTER_DAYS).
  • Socket auth (#290) adds SOCKETCLUSTER_AUTH_ENABLED (default false), SOCKETCLUSTER_AUTH_KEY, SOCKETCLUSTER_PUBLISH_URL (default http://{SOCKETCLUSTER_HOST}:8001), SOCKETCLUSTER_TOKEN_TTL (default 900) and SOCKETCLUSTER_ORIGIN to broadcasting.connections.socketcluster. Set SOCKETCLUSTER_ORIGIN to the console origin when the socket server restricts origins. Nothing changes for socket clients until SOCKETCLUSTER_AUTH_ENABLED=true. Roll out in this order:
    1. Ship clients that fall back to connecting without a token when the token route answers 404.
    2. Set SOCKETCLUSTER_AUTH_ENABLED=true on the API and the socket server, with the socket server in log mode.
    3. Switch the socket server to enforce.
  • Extensions: a relation that clients request through with, expand or counts must declare an Eloquent Relation return type or be listed in $withable, or it stops loading (#303). Every dynamic: notifiable an extension resolves must be passed to registerNotifiable() (#304). Models whose cached queries eager-load other tables should declare $apiCacheDependencies, and writes that skip Eloquent events need an explicit invalidation (#301). fleetbase/fleetops v0.6.72 and fleetbase/storefront v0.4.25 carry these and require this release (fleetbase/core-api ^1.6.69).
  • Public API clients: GET v1/comments and GET v1/comments/{id} no longer return internal comments, POST v1/comments accepts visibility, and comment, chat participant and chat message responses gain fields (#306).
  • To make the media bucket private, remove any public s3:GetObject statement from the bucket policy and turn on Block Public Access (#287).
  • Database backups replace the old S3 backup settings; configure them with DB_BACKUP_* or in the admin settings (#288).

What's Changed

  • fix(files): support a fully private S3 media bucket by @roncodes in #287
  • feat(backups): settings-driven database backups that fail loudly by @roncodes in #288
  • feat(verification): hashed one-time codes with attempt counting by @roncodes in #289
  • perf: cache the country lookup and index files.subject_uuid by @roncodes in #291
  • fix(backups): queue the manual backup through the bus dispatcher by @roncodes in #295
  • feat(socket-auth): authenticated realtime channels (tokens, authorizer, signed publish) by @roncodes in #290
  • fix: no default backup bucket; example bucket names in tests by @roncodes in #296
  • fix(comments): a failed comment publish no longer leaves every model unguarded by @roncodes in #298
  • fix(api-cache): invalidation changes list, model and relationship cache keys by @roncodes in #299
  • fix(api-cache): list cache keys cover the request path and session scope by @roncodes in #300
  • feat(api-cache): writes invalidate every cache that embeds the written table by @roncodes in #301
  • fix(api-logs): log every route that opts in to LogApiRequests and let extensions relate ids by @roncodes in #302
  • fix(api): only real relations can be requested through with, expand and counts by @roncodes in #303
  • fix(notifications): resolve only registered notifiables from saved notification settings by @roncodes in #304
  • feat(sms): guard every SMS send with destination validation, budgets and a circuit breaker by @roncodes in #305
  • Fix driver uuid broadcast channel; comment visibility and comment.created; chat participant roles and system message meta by @roncodes in #306
  • feat(sms): ledger, hashed single-use codes, learned breaker signals and the admin API by @roncodes in #307
  • test(auth): drop the authenticateSmsCode tests the method's removal left behind by @roncodes in #308
  • docs(release): v1.6.69 notes cover everything merged since #291 by @roncodes in #309
  • release: v1.6.69 by @roncodes in #294

Full Changelog: v1.6.68...v1.6.69