diff --git a/RELEASE.md b/RELEASE.md index 92381f23..d6d11976 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -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`).