Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,51 @@
# v1.6.69 — Authenticated realtime channels, private media, database backups and hashed codes
# v1.6.69 — SMS send guard, authenticated realtime channels, API cache invalidation, comment visibility and database backups

## Added

- **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, 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. (#290)
- **Database backups.** Settings-driven backups (`db:backup`) on a configurable schedule, with environment defaults in `config/database-backups.php` (`DB_BACKUP_*`) and an admin override. Failures email the configured addresses and are logged. (#288)
- **Hashed one-time codes.** `VerificationCode::issue()` stores an HMAC of the code and returns the plain code once. `check()` counts attempts and reports `valid`, `invalid`, `expired` or `locked`. The existing generators are unchanged. (#289)
- **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` table (#288) and the `files.subject_uuid` index (#291).
- Socket auth (#290) adds `SOCKETCLUSTER_AUTH_ENABLED` (default `false`), `SOCKETCLUSTER_AUTH_KEY`, `SOCKETCLUSTER_PUBLISH_URL` (default `http://{SOCKETCLUSTER_HOST}:8001`) and `SOCKETCLUSTER_TOKEN_TTL` (default 900) to `broadcasting.connections.socketcluster`. Nothing changes for socket clients until `SOCKETCLUSTER_AUTH_ENABLED=true`. Roll out in this order:
- 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).
- fleetbase/storefront v0.4.25 and fleetbase/fleetops#358 require this release (`fleetbase/core-api ^1.6.69`).
Loading