From 114198f1d9e34eb7fdc60110484085379c75eb3a Mon Sep 17 00:00:00 2001 From: liangxu Date: Sat, 11 Jul 2026 01:02:25 +0800 Subject: [PATCH] docs(auth): document atomic per-type device limit enforcement Clarify in API/README that device limits are enforced independently per normalized device type with atomic overflow revocation, and add a mapper test covering the RFC3339 UTC timestamp contract. Co-Authored-By: Claude Opus 4.8 (1M context) --- server/API.md | 2 ++ server/README.md | 2 +- server/internal/logic/auth_mapper_test.go | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 server/internal/logic/auth_mapper_test.go diff --git a/server/API.md b/server/API.md index 69cf8f1..7c448fe 100644 --- a/server/API.md +++ b/server/API.md @@ -215,6 +215,8 @@ Returns every non-revoked, non-expired web session for the authenticated account `limits` is sourced from `Auth.DeviceLimits.Desktop` and `Auth.DeviceLimits.Mobile` in service configuration. It is returned with the device list so clients display the effective policy without hard-coded counts. Defaults are 2 desktop web sessions and 1 mobile web session. +The limits are enforced independently by normalized device type. Session creation and same-type overflow revocation run in one atomic store operation. When a new login exceeds a limit, the newest login remains active and the oldest same-type session is revoked immediately. Device listing also reconciles sessions created before this enforcement was deployed, preserving the current session first. + Each signed access and refresh token carries an opaque random session ID. Bearer authentication validates that server-side session before accepting the token, so revoked sessions stop working immediately. Device metadata is derived from request headers for display only and is never an authorization signal. A session is reported online when it is current or was active during the previous five minutes. `lastSeenAt` is an RFC3339 UTC instant. Clients must render it in the browser's local timezone rather than displaying the UTC wire representation directly. diff --git a/server/README.md b/server/README.md index a4d98ac..be83815 100644 --- a/server/README.md +++ b/server/README.md @@ -81,7 +81,7 @@ ObjectStorage: ExpiresIn: 900 ``` -`Auth.DeviceLimits` is the source of truth for the desktop and mobile web-session counts shown in account device management. The effective values are returned by `GET /api/account/devices`. +`Auth.DeviceLimits` is the source of truth for desktop and mobile web-session limits. The auth store enforces each device type independently: a new login keeps the newest session and revokes the oldest overflow session atomically. The effective values are returned by `GET /api/account/devices` for display. `Agent.Image.InputImageTransport` controls how input/reference images are sent to the image model for edit/image-to-image calls: `file` uploads image bytes with multipart form data, while `url` sends `images: [{"image_url": "..."}]` for gateways that can fetch public URLs directly. diff --git a/server/internal/logic/auth_mapper_test.go b/server/internal/logic/auth_mapper_test.go new file mode 100644 index 0000000..d162a35 --- /dev/null +++ b/server/internal/logic/auth_mapper_test.go @@ -0,0 +1,16 @@ +package logic + +import ( + "testing" + "time" + + authmodule "img_infinite_canvas/internal/modules/auth" +) + +func TestAccountDeviceMapperReturnsTimezoneNeutralTimestamp(t *testing.T) { + lastSeenAt := time.Date(2026, 7, 11, 16, 51, 0, 0, time.FixedZone("CST", 8*60*60)) + devices := toAPIAccountDevices([]authmodule.Device{{ID: "session-1", LastSeenAt: lastSeenAt}}) + if len(devices) != 1 || devices[0].LastSeenAt != "2026-07-11T08:51:00Z" { + t.Fatalf("expected RFC3339 UTC timestamp, got %#v", devices) + } +}