fix(api): bind daily and synastry charts to stored profiles
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

Stop accepting client-supplied birth data on those paths, and cap session writes plus location lookups so a logged-in caller cannot farm compute.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-19 19:39:01 +08:00
co-authored by Cursor
parent 40a595045e
commit c38f11dbd3
16 changed files with 548 additions and 39 deletions
@@ -63,7 +63,13 @@ test("relationship intent selects domain-specific evidence instead of treating e
assert.match(source, /亲友\/家庭/);
assert.match(source, /其他关系/);
assert.match(source, /relationshipType: SynastryRelationshipType/);
assert.match(source, /body: JSON\.stringify\(\{ selfProfile: profile, partnerProfile: record\.profile, relationshipType \}\)/);
assert.match(source, /body: JSON\.stringify\(\{ partnerChartProfileId: record\.id, relationshipType \}\)/);
assert.doesNotMatch(source, /selfProfile: profile, partnerProfile: record\.profile/);
assert.match(route, /partnerChartProfileId/);
assert.match(route, /from\("profiles"\)/);
assert.match(route, /from\("chart_profiles"\)/);
assert.match(route, /eq\("role", "other"\)/);
assert.match(route, /consumeUserRequestRateLimit\("synastry"/);
assert.match(route, /relationshipType === "business"/);
assert.match(route, /divisions: \["D2", "D10", "D11"\]/);
assert.match(route, /"D10_Dasamsa"/);
+22
View File
@@ -77,6 +77,24 @@ test("owner or validation failures are not retried", async () => {
assert.equal(attempts, 1);
});
test("session writes reject oversized transcripts before they reach storage", () => {
const oversized = chatSessionWriteSchema.safeParse({
...values,
messages: [{ role: "user", text: "字".repeat(16_001) }],
});
const tooMany = chatSessionWriteSchema.safeParse({
...values,
messages: Array.from({ length: 201 }, () => ({ role: "user" as const, text: "你好" })),
});
const tooMuchText = chatSessionWriteSchema.safeParse({
...values,
messages: Array.from({ length: 20 }, () => ({ role: "user" as const, text: "字".repeat(12_000) })),
});
assert.equal(oversized.success, false);
assert.equal(tooMany.success, false);
assert.equal(tooMuchText.success, false);
});
test("session API owns create and update while answer UI keeps sync failures out of reply errors", () => {
const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const collectionRoute = readFileSync(new URL("../src/app/api/sessions/route.ts", import.meta.url), "utf8");
@@ -87,6 +105,10 @@ test("session API owns create and update while answer UI keeps sync failures out
assert.match(collectionRoute, /export async function POST/);
assert.match(itemRoute, /export async function PATCH/);
assert.match(itemRoute, /\.eq\("user_id", user\.id\)/);
assert.match(collectionRoute, /readChatSessionJson/);
assert.match(itemRoute, /readChatSessionJson/);
assert.match(collectionRoute, /ChatSessionBodyTooLargeError/);
assert.match(itemRoute, /ChatSessionBodyTooLargeError/);
});
@@ -269,6 +269,11 @@ test("the homepage card is engine-backed first, with Agent polish off the reques
assert.match(route, /supabase\.auth\.getUser\(\)/);
assert.match(route, /\{ status: "unauthenticated" \}, \{ status: 401 \}/);
assert.match(route, /from\("profiles"\)/);
assert.match(route, /globalBirthProfileFromAccountRow/);
assert.match(route, /consumeUserRequestRateLimit\("dailyStarlanguage"/);
assert.doesNotMatch(route, /body\?\.profile/);
assert.doesNotMatch(route, /body\?\.today/);
assert.match(route, /dailyStarlanguageCacheKey\(user\.id, JSON\.stringify\(payload\), today\)/);
assert.match(route, /pending\(\)\.get\(key\)/);
assert.match(route, /"\/api\/daily_guidance"/);
@@ -289,6 +294,9 @@ test("the homepage card is engine-backed first, with Agent polish off the reques
for (const source of [route, page]) {
assert.doesNotMatch(source, /先收束,再推进|执行力比灵感更重要|适合观察资源流向/);
}
assert.match(page, /fetchDailyStarlanguage\(controller\.signal\)/);
assert.match(page, /body: JSON\.stringify\(\{\}\)/);
assert.doesNotMatch(page, /JSON\.stringify\(\{ profile, today \}\)/);
assert.doesNotMatch(page, /buildDailyStarlanguageCard/);
assert.match(route, /status: "unavailable"/);
assert.match(page, /今天的星语还没写出来/);
@@ -129,3 +129,12 @@ test("birth time cannot be submitted without a birth date", () => {
assert.equal(birthLocationSearchQuerySchema.safeParse({ q: "Paris", birthTime: "05:30" }).success, false);
assert.equal(birthLocationSearchQuerySchema.safeParse({ q: "Paris", birthDate: "2021-02-29" }).success, false);
});
test("location search and timezone routes bound logged-in callers before spending upstream quota", () => {
const searchRoute = readFileSync(new URL("../src/app/api/locations/search/route.ts", import.meta.url), "utf8");
const timezoneRoute = readFileSync(new URL("../src/app/api/locations/timezone/route.ts", import.meta.url), "utf8");
assert.match(searchRoute, /consumeUserRequestRateLimit\("locationSearch"/);
assert.match(timezoneRoute, /consumeUserRequestRateLimit\("locationTimezone"/);
assert.match(searchRoute, /Retry-After/);
assert.match(timezoneRoute, /Retry-After/);
});
+48
View File
@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import test from "node:test";
import { consumeRequestRateLimit } from "../src/lib/request-rate-limit.ts";
test("a user is allowed up to the window limit and then must wait", () => {
const store = new Map<string, number[]>();
const first = consumeRequestRateLimit({
key: "search:user-1",
limit: 2,
windowMs: 60_000,
now: 1_000,
store,
});
const second = consumeRequestRateLimit({
key: "search:user-1",
limit: 2,
windowMs: 60_000,
now: 2_000,
store,
});
const blocked = consumeRequestRateLimit({
key: "search:user-1",
limit: 2,
windowMs: 60_000,
now: 3_000,
store,
});
const otherUser = consumeRequestRateLimit({
key: "search:user-2",
limit: 2,
windowMs: 60_000,
now: 3_000,
store,
});
const afterWindow = consumeRequestRateLimit({
key: "search:user-1",
limit: 2,
windowMs: 60_000,
now: 61_500,
store,
});
assert.deepEqual(first, { ok: true });
assert.deepEqual(second, { ok: true });
assert.deepEqual(blocked, { ok: false, retryAfterSeconds: 58 });
assert.deepEqual(otherUser, { ok: true });
assert.deepEqual(afterWindow, { ok: true });
});
@@ -0,0 +1,76 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
globalBirthProfileFromAccountRow,
globalBirthProfileFromStoredChart,
} from "../src/lib/server-owned-birth-profile.ts";
test("accepted account rows use the server active minute, not a reported leftover", () => {
const profile = globalBirthProfileFromAccountRow({
name: "测试",
birth_date: "1990-06-15",
reported_birth_time: "08:00:00",
active_birth_time: "09:15:00",
birth_time_status: "accepted",
country_code: "CN",
province_code: "310000",
city_code: "310100",
latitude: 31.2,
longitude: 121.5,
timezone_offset: 8,
timezone_id: "Asia/Shanghai",
});
assert.equal(profile.date, "1990-06-15");
assert.equal(profile.time, "09:15");
assert.equal(profile.timezoneId, "Asia/Shanghai");
assert.equal(profile.birthTimeStatus, "accepted");
});
test("reported rows keep the declared clock until an active minute exists", () => {
const profile = globalBirthProfileFromAccountRow({
birth_date: new Date("1991-01-02T00:00:00.000Z"),
reported_birth_time: "07:40",
active_birth_time: null,
birth_time_status: "reported",
});
assert.equal(profile.date, "1991-01-02");
assert.equal(profile.time, "07:40");
});
test("stored other-chart JSON keeps the library camelCase shape", () => {
const profile = globalBirthProfileFromStoredChart({
name: "对方",
date: "1988-03-04",
time: "18:20",
reportedTime: "18:20",
countryCode: "CN",
provinceCode: "110000",
cityCode: "110100",
latitude: 39.9,
longitude: 116.4,
timezoneOffset: 8,
timezoneId: "Asia/Shanghai",
});
assert.deepEqual(profile, {
name: "对方",
date: "1988-03-04",
time: "18:20",
countryCode: "CN",
provinceCode: "110000",
cityCode: "110100",
districtCode: undefined,
latitude: 39.9,
longitude: 116.4,
timezoneOffset: 8,
timezoneId: "Asia/Shanghai",
birthTimeStatus: undefined,
});
});
test("a stored chart without a clock cannot be used for synastry", () => {
assert.equal(globalBirthProfileFromStoredChart({ date: "1990-01-01", name: "对方" }), null);
assert.equal(globalBirthProfileFromStoredChart(null), null);
});