@cosmicdrift/kumiko-framework 0.60.0 → 0.60.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.60.0",
3
+ "version": "0.60.1",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -204,6 +204,34 @@ describe("auth-routes cookieDomain", () => {
204
204
  expect(getSetCookieRaw(res, CSRF_COOKIE_NAME)).toMatch(/Domain=example\.eu/i);
205
205
  });
206
206
 
207
+ // Symmetrisch zum Logout (#321/1): wer mit cookieDomain SETZT, muss die
208
+ // host-only-Variante mit-invalidieren — sonst koexistiert nach dem ersten
209
+ // Deploy mit cookieDomain das alte host-only-Cookie neben dem neuen
210
+ // Domain-Cookie und der Server liest umgebungsabhängig das veraltete.
211
+ test("login: cookieDomain invalidiert zusätzlich die host-only-Variante", async () => {
212
+ const { app } = await buildApp({ cookieDomain: "example.eu" });
213
+ const raw = (await login(app)).headers.getSetCookie();
214
+ const authCookies = raw.filter((c) => c.startsWith(`${AUTH_COOKIE_NAME}=`));
215
+ const csrfCookies = raw.filter((c) => c.startsWith(`${CSRF_COOKIE_NAME}=`));
216
+ // host-only-Delete: Max-Age=0 ohne Domain-Attribut.
217
+ expect(authCookies.some((c) => /Max-Age=0/i.test(c) && !/Domain=/i.test(c))).toBe(true);
218
+ expect(csrfCookies.some((c) => /Max-Age=0/i.test(c) && !/Domain=/i.test(c))).toBe(true);
219
+ // ...neben dem echten Domain-Cookie (positive Max-Age).
220
+ expect(authCookies.some((c) => /Domain=example\.eu/i.test(c) && !/Max-Age=0/i.test(c))).toBe(
221
+ true,
222
+ );
223
+ });
224
+
225
+ test("ohne cookieDomain: KEIN zusätzlicher host-only-Delete beim Login", async () => {
226
+ const { app } = await buildApp();
227
+ const raw = (await login(app)).headers.getSetCookie();
228
+ // Nur das eine Set-Cookie pro Name, kein Max-Age=0-Delete.
229
+ expect(raw.filter((c) => c.startsWith(`${AUTH_COOKIE_NAME}=`)).length).toBe(1);
230
+ expect(raw.some((c) => c.startsWith(`${AUTH_COOKIE_NAME}=`) && /Max-Age=0/i.test(c))).toBe(
231
+ false,
232
+ );
233
+ });
234
+
207
235
  test("switch-tenant: rotierte Cookies tragen das Domain-Attribut", async () => {
208
236
  const otherTenant = TestUsers.otherTenant;
209
237
  const dispatcher = createStubDispatcher({
@@ -56,6 +56,16 @@ function setAuthCookies(
56
56
  ...(opts.domain !== undefined && { domain: opts.domain }),
57
57
  } as const;
58
58
 
59
+ // Bei gesetzter Domain zuerst die host-only-Variante invalidieren (analog
60
+ // clearAuthCookies): sonst koexistiert nach einem Deploy mit neuem
61
+ // cookieDomain das alte host-only-Cookie mit dem neuen Domain-Cookie
62
+ // (RFC 6265: name+domain ist distinct) und der Server bindet
63
+ // umgebungsabhängig potenziell ans veraltete host-only-Token.
64
+ if (opts.domain !== undefined) {
65
+ deleteCookie(c, AUTH_COOKIE_NAME, { path: "/" });
66
+ deleteCookie(c, CSRF_COOKIE_NAME, { path: "/" });
67
+ }
68
+
59
69
  setCookie(c, AUTH_COOKIE_NAME, opts.token, { ...common, httpOnly: true });
60
70
  // Intentionally NOT HttpOnly — the web client has to read this from
61
71
  // document.cookie to include it in the X-CSRF-Token request header.
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createDecimalField, createMoneyField, createNumberField } from "../factories";
3
+ import { getAllowedFilterOps, isFieldFilterable } from "../screen-filter-ops";
4
+ import type { ScreenFilterOp } from "../types";
5
+
6
+ const COMPARABLE: ScreenFilterOp[] = ["eq", "ne", "lt", "gt", "in"];
7
+
8
+ describe("getAllowedFilterOps — decimal is comparable (#343/1)", () => {
9
+ test("decimal yields the full comparable op-set, not the empty default", () => {
10
+ const ops = getAllowedFilterOps(createDecimalField({ precision: 10, scale: 2 }));
11
+ expect([...ops].sort()).toEqual([...COMPARABLE].sort());
12
+ expect(ops.length).toBeGreaterThan(0);
13
+ });
14
+
15
+ test("decimal matches number/money — same comparable surface", () => {
16
+ const decimal = getAllowedFilterOps(createDecimalField({ precision: 6, scale: 2 }));
17
+ const number = getAllowedFilterOps(createNumberField({}));
18
+ const money = getAllowedFilterOps(createMoneyField({}));
19
+ expect([...decimal].sort()).toEqual([...number].sort());
20
+ expect([...decimal].sort()).toEqual([...money].sort());
21
+ });
22
+
23
+ test("a filterable decimal field is usable: filterable AND non-empty ops", () => {
24
+ const field = createDecimalField({ precision: 8, scale: 2, filterable: true });
25
+ expect(isFieldFilterable(field)).toBe(true);
26
+ // Regression guard: before the fix this returned [] → the boot-validator
27
+ // rejected EVERY filter op on a filterable decimal field ("Allowed ops:
28
+ // (none)"), making the field unusable.
29
+ expect(getAllowedFilterOps(field).length).toBeGreaterThan(0);
30
+ });
31
+ });
@@ -6,8 +6,8 @@
6
6
  // compare nutzt der Author selten bewusst); Author kann den
7
7
  // Filter dann einfach nicht setzen.
8
8
  // - boolean: eq, ne (in/lt/gt sinnlos für 2-Werte-Type).
9
- // - number/money/date/timestamp/locatedTimestamp: alle 5 Ops —
10
- // die Felder sind natürlich vergleichbar.
9
+ // - number/money/decimal/date/timestamp/locatedTimestamp: alle 5
10
+ // Ops — die Felder sind natürlich vergleichbar.
11
11
  //
12
12
  // Boot-Validator nutzt diese Map um Author-Fehler früh zu fangen
13
13
  // ("filter mit op:lt auf einem text-Feld" → Boot-Fail). Erweitert sich
@@ -30,6 +30,7 @@ export function getAllowedFilterOps(field: FieldDefinition): readonly ScreenFilt
30
30
  return BOOL_OPS;
31
31
  case "number":
32
32
  case "money":
33
+ case "decimal":
33
34
  case "date":
34
35
  case "timestamp":
35
36
  case "locatedTimestamp":