@cosmicdrift/kumiko-renderer-web 0.156.0 → 0.156.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-renderer-web",
3
- "version": "0.156.0",
3
+ "version": "0.156.1",
4
4
  "description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -16,9 +16,9 @@
16
16
  "./styles.css": "./src/styles.css"
17
17
  },
18
18
  "dependencies": {
19
- "@cosmicdrift/kumiko-dispatcher-live": "0.156.0",
20
- "@cosmicdrift/kumiko-headless": "0.156.0",
21
- "@cosmicdrift/kumiko-renderer": "0.156.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.156.1",
20
+ "@cosmicdrift/kumiko-headless": "0.156.1",
21
+ "@cosmicdrift/kumiko-renderer": "0.156.1",
22
22
  "@radix-ui/react-dialog": "^1.1.15",
23
23
  "@radix-ui/react-dropdown-menu": "^2.1.16",
24
24
  "@radix-ui/react-label": "^2.1.8",
@@ -6,7 +6,12 @@ import type {
6
6
  EntityListScreenDefinition,
7
7
  } from "@cosmicdrift/kumiko-framework/ui-types";
8
8
  import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
9
- import type { ColumnRendererProps, FeatureSchema, NavApi } from "@cosmicdrift/kumiko-renderer";
9
+ import type {
10
+ AppSchema,
11
+ ColumnRendererProps,
12
+ FeatureSchema,
13
+ NavApi,
14
+ } from "@cosmicdrift/kumiko-renderer";
10
15
  import { createStaticLocaleResolver } from "@cosmicdrift/kumiko-renderer";
11
16
  import { act, screen, waitFor } from "@testing-library/react";
12
17
  import type { ReactNode } from "react";
@@ -150,6 +155,54 @@ describe("createKumikoApp", () => {
150
155
  );
151
156
  });
152
157
 
158
+ test("fallback screen skips access-restricted screens across features (#1176)", async () => {
159
+ mountRoot();
160
+ const restrictedFeature: FeatureSchema = {
161
+ featureName: "user",
162
+ entities: { task: taskEntity },
163
+ screens: [{ ...listScreen, id: "user-list", access: { roles: ["SystemAdmin"] } }],
164
+ };
165
+ const openFeature: FeatureSchema = { ...baseSchema, featureName: "tasks" };
166
+ const schema: AppSchema = { features: [restrictedFeature, openFeature] };
167
+ await mountApp({ schema, dispatcher: makeDispatcher() });
168
+ // Landing on user-list (SystemAdmin-only) would show an access-denied
169
+ // banner instead — the tasks:task-edit form must win instead.
170
+ await waitFor(() => expect(screen.getByTestId("render-edit-form")).toBeTruthy());
171
+ });
172
+
173
+ test("fallback screen: an openToAll screen counts as accessible", async () => {
174
+ mountRoot();
175
+ const restrictedFeature: FeatureSchema = {
176
+ featureName: "user",
177
+ entities: { task: taskEntity },
178
+ screens: [{ ...editScreen, id: "user-edit", access: { roles: ["SystemAdmin"] } }],
179
+ };
180
+ const openFeature: FeatureSchema = {
181
+ featureName: "tasks",
182
+ entities: { task: taskEntity },
183
+ screens: [{ ...listScreen, access: { openToAll: true } }],
184
+ };
185
+ const schema: AppSchema = { features: [restrictedFeature, openFeature] };
186
+ await mountApp({ schema, dispatcher: makeDispatcher() });
187
+ expect(await screen.findByTestId("render-list-table-empty")).toBeTruthy();
188
+ });
189
+
190
+ test("all screens access-restricted → throws (no accessible fallback)", () => {
191
+ mountRoot();
192
+ const restrictedOnly: AppSchema = {
193
+ features: [
194
+ {
195
+ featureName: "user",
196
+ entities: { task: taskEntity },
197
+ screens: [{ ...editScreen, access: { roles: ["SystemAdmin"] } }],
198
+ },
199
+ ],
200
+ };
201
+ expect(() => createKumikoApp({ schema: restrictedOnly, dispatcher: makeDispatcher() })).toThrow(
202
+ /no screens/,
203
+ );
204
+ });
205
+
153
206
  test("clientFeatures.columnRenderers → bei Key-Kollision warnt + last-wins gewinnt", async () => {
154
207
  // Zwei Features liefern denselben Renderer-Key — der Merge in
155
208
  // create-app warnt und behält den späteren Eintrag (Last-Wins).
@@ -129,6 +129,22 @@ function readInjectedSchema(): AppSchema | FeatureSchema | undefined {
129
129
  return w.__KUMIKO_SCHEMA__;
130
130
  }
131
131
 
132
+ // Erstes Screen über alle Features in deklarierter Reihenfolge, dessen
133
+ // `access` niemanden ausschließt (undefined oder openToAll). Die Landing-
134
+ // Route wird VOR Auth-Resolution gewählt, kennt also keine User-Rollen —
135
+ // ein role-restricted Screen (z.B. bundled user/tenant, SystemAdmin-only)
136
+ // darf hier nie gewinnen, sonst landet jeder Nicht-Admin auf einem
137
+ // Access-Denied-Screen (#1176).
138
+ function firstOpenScreenQn(features: readonly FeatureSchema[]): string | undefined {
139
+ for (const feature of features) {
140
+ const openScreen = feature.screens.find(
141
+ (s) => s.access === undefined || "openToAll" in s.access,
142
+ );
143
+ if (openScreen !== undefined) return qualifyScreenId(feature.featureName, openScreen.id);
144
+ }
145
+ return undefined;
146
+ }
147
+
132
148
  export function createKumikoApp(options: CreateKumikoAppOptions = {}): { readonly root: Root } {
133
149
  const rootId = options.rootId ?? "root";
134
150
  const container = document.getElementById(rootId);
@@ -154,19 +170,11 @@ export function createKumikoApp(options: CreateKumikoAppOptions = {}): { readonl
154
170
  }
155
171
  const app = toAppSchema(rawSchema);
156
172
 
157
- // Fallback-Screen: erstes Screen über alle Features in deklarierter
158
- // Reihenfolge. War vorher schema.screens[0], jetzt search the first
159
- // feature with screens.
160
- const firstFeatureWithScreens = app.features.find((f) => f.screens.length > 0);
161
- const firstScreen = firstFeatureWithScreens?.screens[0];
162
- const fallbackQn =
163
- options.screenQn ??
164
- (firstScreen !== undefined && firstFeatureWithScreens !== undefined
165
- ? qualifyScreenId(firstFeatureWithScreens.featureName, firstScreen.id)
166
- : undefined);
173
+ // Fallback-Screen falls kein explizites screenQn übergeben wurde.
174
+ const fallbackQn = options.screenQn ?? firstOpenScreenQn(app.features);
167
175
  if (!fallbackQn) {
168
176
  throw new Error(
169
- "createKumikoApp: schema contains no screens. Add at least one entry to `schema.screens` or pass `screenQn` explicitly.",
177
+ "createKumikoApp: schema contains no screens accessible without a role restriction. Add at least one entry to `schema.screens` without `access.roles`, or pass `screenQn` explicitly.",
170
178
  );
171
179
  }
172
180