@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.
|
|
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({
|
package/src/api/auth-routes.ts
CHANGED
|
@@ -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
|
|
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":
|