clone

an rts arena for scripts: submit tribe strategies over the web or mcp, run deterministic matches, watch them

feat: display-name changes carry a 7-day cooldown

Dami committed 46dbdaf
Names are leaderboard identity; churn would make records unattributable.
An effective change (including clears) stamps name_changed_at; further
changes inside 7 days get a bad_input with the unlock time, same-value
writes are no-ops, and the sharing toggle is unaffected. UserInfo exposes
nameLockedUntil; the Account card shows the lock.
commit
46dbdaf130b19acc3d476dc43869b1650c18af2f
parent
d42fe89fbd18d654acea8e85a04b6428af91b61b
author
Dami <dami.etoile@gmail.com>
9 files changed, +74 -5
Amigrations/0007_name_cooldown.sql4++++
Mpackages/protocol/src/api.ts2++
Mweb/src/pages/connect.ts8+++++++-
Mworker/src/db.ts7++++++-
Mworker/src/mcp/handler.test.ts1+
Mworker/src/router.ts8++++++++
Mworker/src/service.ts27+++++++++++++++++++++++++--
Mworker/test/fake-d1.ts2+-
Mworker/test/social.test.ts20++++++++++++++++++++
A migrations/0007_name_cooldown.sql +4 -0
M packages/protocol/src/api.ts +2 -0
M web/src/pages/connect.ts +7 -1
M worker/src/db.ts +6 -1
M worker/src/mcp/handler.test.ts +1 -0
M worker/src/router.ts +8 -0
M worker/src/service.ts +25 -2
@@ -157,12 +157,21 @@ function toRef(r: db.SimScriptRow): SimScriptRef {
157157 };158158 }159159 160-function userInfoOf(row: db.UserRow): UserInfo {160+const NAME_COOLDOWN_SECONDS = 7 * 24 * 3600;161+162+function nameLockedUntilOf(row: db.UserRow, now: number): string | null {163+ if (row.name_changed_at === null) return null;164+ const until = row.name_changed_at + NAME_COOLDOWN_SECONDS;165+ return until > now ? isoOf(until) : null;166+}167+168+function userInfoOf(row: db.UserRow, now = nowSec()): UserInfo {161169 return {162170 id: row.id,163171 externalId: row.external_id,164172 name: row.name,165173 publicSharing: row.public_sharing === 1,174+ nameLockedUntil: nameLockedUntilOf(row, now),166175 };167176 }168177
@@ -431,8 +440,22 @@ export function createService(env: Env, deps: ServiceDeps): TribesService {
431440 await db.setUserSharing(D, user.userId, input.publicSharing);432441 }433442 if (input.name !== undefined) {443+ const now = nowSec();444+ const current = await db.getUserById(D, user.userId);445+ if (!current) throw new ServiceError('not_found', 'user no longer exists');434446 const name = input.name === null ? null : sanitizeMeta(input.name, 40);435- await db.updateUserName(D, user.userId, name === '' ? null : name);447+ const next = name === '' ? null : name;448+ // Same value = no-op; an effective change consumes the 7-day cooldown.449+ if (next !== current.name) {450+ const lockedUntil = nameLockedUntilOf(current, now);451+ if (lockedUntil !== null) {452+ throw new ServiceError(453+ 'bad_input',454+ `display name changes are limited to once per 7 days (next change ${lockedUntil})`,455+ );456+ }457+ await db.updateUserName(D, user.userId, next, now);458+ }436459 }437460 const row = await db.getUserById(D, user.userId);438461 if (!row) throw new ServiceError('not_found', 'user no longer exists');
M worker/test/fake-d1.ts +1 -1
M worker/test/social.test.ts +20 -0