feat(sub): add read-only HWID device-slot status endpoint - #6380
Conversation
Closes MHSanaei#6357 A client with an HWID limit had no way to tell a subscriber how many device slots were left: /{subPath}/{subId} only exposes the gate as a boolean through X-Hwid-* headers on a 404, and ?format=info carries no limitHwid or registered count. Every "why can't I connect on my new phone" case therefore had to be answered by the operator by hand. GET /{subPath}/{subId}/hwid-status now returns the aggregate counters: {"active":true,"limit":2,"registered":1,"remaining":1,"full":false} - SELECT-only. It never registers an hwid, never touches last_seen and never calls the enforcement path, so asking about a slot cannot spend one. - Counters only: no hwid value or hash, no email, no device metadata, no IP, no User-Agent, and none of the X-Hwid-* gate headers. - The subscription id is already the bearer secret for /{subPath}/{subId}, so no admin token and no new auth mechanism. - Unknown and disabled subscriptions both answer a bare 404, with identical status, headers and body, so the route cannot be used to probe which subscription ids exist. - No HWID limit configured returns {"active":false,"limit":0,...}. - No schema change and no migration. Scoped to enabled clients exactly like effectiveHwidLimitForSubID, so the reported limit is always the limit the gate enforces on a shared sub_id, and remaining clamps at zero when the effective limit drops below the number of registered devices. A separate route leaves /{subPath}/{subId}, ?format=info and the JSON/Clash routes byte-for-byte unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code reviewNo blocking issues — The change is additive and self-consistent: a SELECT-only handler, a read-only service method, two route registrations, tests, and the full regenerated doc chain. Nothing this repo treats as Important came back dirty. One thing to settle before merge that is not a code defect: no CI has run on this head. All four workflows on Checked and cleared
Coverage — head
|
CI evidence for
|
Closes #6357
What
Adds one additive, public, read-only endpoint on the subscription router, as requested in #6357:
A subscriber (or a customer-facing panel / reseller bot built on the subscription API) can now show "Devices 1/2 — 1 slot remaining" instead of a silently failing connection.
Behaviour
last_seen, and never callsEnforceHwidForSubID, so asking about a slot cannot spend one. A test assertsclient_hwidsstays empty after a probe that carriesX-HWID, and that the next real fetch with that same HWID still succeeds.X-Hwid-*gate headers. The test decodes into a map and asserts the exact field set, so an added field fails./{subPath}/{subId}.404, and a test asserts identical status, headers and body.{"active":false,"limit":0,"registered":0,"remaining":0,"full":false}./{subPath}/{subId},?format=info, and the JSON/Clash routes are not modified at all.Notes for reviewers
HwidSlotStatusForSubIDscopes the limit through the existingeffectiveHwidLimitForSubIDhelper (enabled clients,MAX(limit_hwid)), so the reported limit is always the enforced limit on a sharedsub_id. That is also why existence is "≥1 enabled client with this sub_id" — the same notion the gate uses — rather than the body route's "enabled inbound joined to the sub_id".remainingclamps at zero: deleting or disabling the highest-limit client of a sharedsub_idlowers the effectiveMAXwithout trimming rows, soregistered > limitis reachable. Covered by a test.HEADis registered alongsideGET, matching the sibling subscription routes.frontend/src/pages/api-docs/endpoints.ts,make genwas run, andfrontend/public/openapi.json→docs/public/openapi.json+cd docs && pnpm gen:apiwere regenerated. The response is documented in prose becausebuild-openapi.mjswraps every operation in the{success,msg,obj}panel envelope, which does not apply to subscription-server routes.How to test
Manually, with a client that has
limitHwid = 1:Checks run locally
go build ./...,go vet,go test -shuffle=on -count=1 ./...,go test -race -shuffle=on ./internal/sub/ ./internal/web/service/, frontendtypecheck/lint/format:check, andmake genreproducing the committed generated files. (internal/mtproto's two process-spawn timing tests flaked once on a loaded laptop and pass on a re-run; they are untouched by this change.)