@cosmicdrift/kumiko-types 0.319.0 → 0.320.0
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 +1 -1
- package/src/__tests__/handlers.test.ts +40 -0
- package/src/changes.json +6 -0
- package/src/config.ts +4 -0
- package/src/handlers.ts +18 -0
- package/src/http-route.ts +6 -0
- package/src/screen.ts +12 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.320.0",
|
|
4
4
|
"description": "Framework-Type-Definitions für Kumiko — FeatureDefinition, BootCheck-Types und die reinen Engine-Types. Erlaubt Downstream-Konsumenten, gegen die Type-Contracts zu bauen, ohne das ganze Framework-Package zu importieren. Enthaelt keine identitaets-sensitiven Runtime-Werte mehr (Error-Klassen leben seit #1629 in kumiko-framework, Brand-Symbole nutzen Symbol.for) und ist deshalb eine plain dependency, keine peerDependency.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { isUiAccessGranted } from "../handlers";
|
|
3
|
+
|
|
4
|
+
describe("isUiAccessGranted", () => {
|
|
5
|
+
test("unset access is visible to every signed-in user", () => {
|
|
6
|
+
expect(isUiAccessGranted(undefined, [])).toBe(true);
|
|
7
|
+
expect(isUiAccessGranted(undefined, undefined)).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("openToAll with a non-empty reason grants regardless of roles", () => {
|
|
11
|
+
expect(isUiAccessGranted({ openToAll: { reason: "any signed-in user" } }, [])).toBe(true);
|
|
12
|
+
expect(isUiAccessGranted({ openToAll: { reason: "any signed-in user" } }, undefined)).toBe(
|
|
13
|
+
true,
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("openToAll with a blank reason denies — malformed/untyped input narrowed defensively", () => {
|
|
18
|
+
expect(isUiAccessGranted({ openToAll: { reason: " " } }, [])).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("roles rule grants on overlap", () => {
|
|
22
|
+
expect(isUiAccessGranted({ roles: ["Admin", "SystemAdmin"] }, ["User", "Admin"])).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("roles rule denies without overlap", () => {
|
|
26
|
+
expect(isUiAccessGranted({ roles: ["Admin"] }, ["User"])).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("roles rule denies when caller has no roles", () => {
|
|
30
|
+
expect(isUiAccessGranted({ roles: ["Admin"] }, [])).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("roles rule denies when caller roles are undefined", () => {
|
|
34
|
+
expect(isUiAccessGranted({ roles: ["Admin"] }, undefined)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("empty roles rule denies everyone (explicit default-deny opt-in)", () => {
|
|
38
|
+
expect(isUiAccessGranted({ roles: [] }, ["Admin"])).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.320.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "ToolbarAction's navigate variant gets `tab`",
|
|
6
|
+
"detail": "The navigate-kind ToolbarAction (entityList, projectionList and\nrelatedList-section toolbars) accepts `tab?: string`, the section id of the\ntab to activate on the target projectionDetail (layout.mode \"tabs\"),\nanalogous to RowActionNavigate.tab and MetricNavigate.tab."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.310.0",
|
|
4
10
|
"type": "improvement",
|
package/src/config.ts
CHANGED
|
@@ -375,6 +375,10 @@ export type JobDefinition = {
|
|
|
375
375
|
readonly name: string;
|
|
376
376
|
readonly handler: JobHandlerFn;
|
|
377
377
|
readonly trigger: JobTrigger;
|
|
378
|
+
// "sequential" is mutual exclusion, not a FIFO queue: same-name dispatches
|
|
379
|
+
// never run concurrently, but a loser of the per-name lock is re-enqueued
|
|
380
|
+
// to the back of its queue, so a job dispatched later can still complete
|
|
381
|
+
// before one dispatched earlier (fw#3265). Don't rely on dispatch order.
|
|
378
382
|
readonly concurrency?: ConcurrencyMode | undefined;
|
|
379
383
|
readonly maxPerTenant?: number | undefined;
|
|
380
384
|
readonly debounceMs?: number | undefined;
|
package/src/handlers.ts
CHANGED
|
@@ -56,6 +56,24 @@ export function isOpenToAllGranted(rule: AccessRule): boolean {
|
|
|
56
56
|
return openToAll.reason.trim().length > 0;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// UI-visibility predicate — DEFAULT-VISIBLE, the opposite stance from
|
|
60
|
+
// `hasAccess`/AccessRule's own default-deny doc above: an unset `access`
|
|
61
|
+
// means every signed-in user may see the screen/nav/workspace, not nobody.
|
|
62
|
+
// The engine's dispatcher access check stays default-deny; this only
|
|
63
|
+
// decides what a client renders/lists, never what a handler accepts.
|
|
64
|
+
// Same `unknown`-narrowing caution as isOpenToAllGranted — `access` can
|
|
65
|
+
// arrive from untyped sources.
|
|
66
|
+
export function isUiAccessGranted(
|
|
67
|
+
access: AccessRule | undefined,
|
|
68
|
+
userRoles: readonly string[] | undefined,
|
|
69
|
+
): boolean {
|
|
70
|
+
if (access === undefined) return true;
|
|
71
|
+
if ("openToAll" in access) return isOpenToAllGranted(access);
|
|
72
|
+
if (userRoles === undefined) return false;
|
|
73
|
+
if (!("roles" in access)) return false;
|
|
74
|
+
return access.roles.some((role) => userRoles.includes(role));
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
// --- Pipeline User ---
|
|
60
78
|
|
|
61
79
|
// Set only on a SessionUser the framework resolved internally for a
|
package/src/http-route.ts
CHANGED
|
@@ -48,6 +48,12 @@ export type HttpRouteHandlerDeps = {
|
|
|
48
48
|
payload: unknown,
|
|
49
49
|
tenantId: import("./identifiers").TenantId,
|
|
50
50
|
) => Promise<unknown>;
|
|
51
|
+
/** Caller IP resolved via the server's configured, trustedProxyHops-aware
|
|
52
|
+
* resolver (same one requestIdMiddleware uses for `/api/*`) — a route
|
|
53
|
+
* handler needing the caller's IP (rate limiting, audit) should read
|
|
54
|
+
* this instead of parsing X-Forwarded-For itself, which trusts
|
|
55
|
+
* whichever hop count the deployment actually has in front of it. */
|
|
56
|
+
readonly clientIp: string;
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
export type HttpRouteHandler = (
|
package/src/screen.ts
CHANGED
|
@@ -79,7 +79,7 @@ export interface FieldFormatRegistry {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// Discriminated union derived from the registry — one variant per key.
|
|
82
|
-
// JSON-safe: no function members, survives buildAppSchema →
|
|
82
|
+
// JSON-safe: no function members, survives buildAppSchema → GET /api/schema.
|
|
83
83
|
export type FormatSpec = {
|
|
84
84
|
[K in keyof FieldFormatRegistry]: { readonly format: K } & FieldFormatRegistry[K];
|
|
85
85
|
}[keyof FieldFormatRegistry];
|
|
@@ -97,7 +97,7 @@ export type FieldRenderer = PlatformComponent | string | FormatSpec;
|
|
|
97
97
|
// { field, ne } — true when row[field] !== ne
|
|
98
98
|
// { field, in } — true when row[field] is one of the given values
|
|
99
99
|
// { field, notIn } — true when row[field] is none of the given values
|
|
100
|
-
// JSON-safe: survives buildAppSchema →
|
|
100
|
+
// JSON-safe: survives buildAppSchema → GET /api/schema stringify.
|
|
101
101
|
export type FieldCondition =
|
|
102
102
|
| boolean
|
|
103
103
|
| { readonly field: string; readonly eq: unknown }
|
|
@@ -196,14 +196,14 @@ export type ScreenFilter = {
|
|
|
196
196
|
readonly value: unknown;
|
|
197
197
|
};
|
|
198
198
|
|
|
199
|
-
/**
|
|
200
|
-
*
|
|
199
|
+
/** Declarative row-field extractor — JSON-safe (no function prop, survives
|
|
200
|
+
* GET /api/schema / buildAppSchema).
|
|
201
201
|
*
|
|
202
|
-
* `pick`:
|
|
203
|
-
* `map`:
|
|
202
|
+
* `pick`: extracts fields 1:1. `{ pick: ["id", "version"] }` → `{ id: row.id, version: row.version }`
|
|
203
|
+
* `map`: renames fields. `{ map: { incidentId: "id" } }` → `{ incidentId: row.id }`
|
|
204
204
|
*
|
|
205
|
-
* Limitation: computed/
|
|
206
|
-
*
|
|
205
|
+
* Limitation: computed/templated values can't be expressed this way —
|
|
206
|
+
* that logic belongs server-side in the write handler. */
|
|
207
207
|
export type RowFieldExtractor =
|
|
208
208
|
| { readonly pick: readonly string[] }
|
|
209
209
|
| { readonly map: Readonly<Record<string, string>> };
|
|
@@ -350,6 +350,10 @@ export type ToolbarAction =
|
|
|
350
350
|
readonly label: string;
|
|
351
351
|
/** Screen-id (kurz, unqualified) zu dem navigiert wird. */
|
|
352
352
|
readonly screen: string;
|
|
353
|
+
/** Section id of the tab to activate on the target projectionDetail
|
|
354
|
+
* (layout.mode "tabs"); merged into the navigate search params next to
|
|
355
|
+
* returnTo. Boot validator checks the id exists on the target. */
|
|
356
|
+
readonly tab?: string;
|
|
353
357
|
/** Visual style only — unlike the writeHandler variant, "danger" does
|
|
354
358
|
* not force a confirm dialog here: the target form is the
|
|
355
359
|
* confirmation. */
|