@cosmicdrift/kumiko-bundled-features 0.313.0 → 0.314.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.313.0",
3
+ "version": "0.314.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -133,12 +133,12 @@
133
133
  "./workflow-runner": "./src/workflow-runner/index.ts"
134
134
  },
135
135
  "dependencies": {
136
- "@cosmicdrift/kumiko-dispatcher-live": "0.313.0",
137
- "@cosmicdrift/kumiko-framework": "0.313.0",
138
- "@cosmicdrift/kumiko-headless": "0.313.0",
139
- "@cosmicdrift/kumiko-renderer": "0.313.0",
140
- "@cosmicdrift/kumiko-renderer-web": "0.313.0",
141
- "@cosmicdrift/kumiko-types": "0.313.0",
136
+ "@cosmicdrift/kumiko-dispatcher-live": "0.314.0",
137
+ "@cosmicdrift/kumiko-framework": "0.314.0",
138
+ "@cosmicdrift/kumiko-headless": "0.314.0",
139
+ "@cosmicdrift/kumiko-renderer": "0.314.0",
140
+ "@cosmicdrift/kumiko-renderer-web": "0.314.0",
141
+ "@cosmicdrift/kumiko-types": "0.314.0",
142
142
  "@mollie/api-client": "^4.5.0",
143
143
  "@node-rs/argon2": "^2.0.2",
144
144
  "@types/mailparser": "^3.4.6",
@@ -167,8 +167,8 @@
167
167
  ],
168
168
  "devDependencies": {
169
169
  "@testing-library/user-event": "^14.6.1",
170
- "@cosmicdrift/kumiko-locale-de": "0.313.0",
171
- "@cosmicdrift/kumiko-locale-es": "0.313.0",
170
+ "@cosmicdrift/kumiko-locale-de": "0.314.0",
171
+ "@cosmicdrift/kumiko-locale-es": "0.314.0",
172
172
  "jsqr": "^1.4.0"
173
173
  }
174
174
  }
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.314.0",
4
+ "type": "improvement",
5
+ "title": "SignupCompleteScreen passes the claimed handover to loggedInHref",
6
+ "detail": "The function form of `loggedInHref` on `SignupCompleteScreen` now receives\nan optional `handover: { entityType, id }` next to `tenantKey`/`roles`,\nmirroring the `handover` field `confirmSignup` already returns when a\ntry-first tenant-handover grant was claimed during signup. Apps can route a\nfreshly activated user straight to the entity they claimed (offlot-app#454)\ninstead of a fixed landing page. The string form and callers that ignore the\nextra field are unaffected."
7
+ },
2
8
  {
3
9
  "version": "0.313.0",
4
10
  "type": "improvement",
@@ -179,6 +179,72 @@ describe("SignupCompleteScreen", () => {
179
179
  });
180
180
  });
181
181
 
182
+ test("confirm-Antwort mit handover → loggedInHref-Function erhält handover", async () => {
183
+ globalThis.fetch = mock(
184
+ async () =>
185
+ new Response(
186
+ JSON.stringify({
187
+ user: { id: "u1", tenantId: "t1", roles: ["User"] },
188
+ tenantKey: "acme",
189
+ handover: { entityType: "run", id: "run-1" },
190
+ }),
191
+ { status: 200, headers: { "Content-Type": "application/json" } },
192
+ ),
193
+ ) as unknown as typeof fetch;
194
+
195
+ renderWithProviders(
196
+ <SignupCompleteScreen
197
+ token="abc-token"
198
+ loggedInHref={({ tenantKey, handover }) =>
199
+ handover !== undefined
200
+ ? `/${tenantKey}/${handover.entityType}/${handover.id}`
201
+ : `/${tenantKey}/`
202
+ }
203
+ />,
204
+ );
205
+ fillPasswords("validpass1", "validpass1");
206
+ fireEvent.click(screen.getByRole("button", { name: "Activate account" }));
207
+
208
+ await waitFor(() => {
209
+ expect(screen.getByRole("link", { name: "Continue" }).getAttribute("href")).toBe(
210
+ "/acme/run/run-1",
211
+ );
212
+ });
213
+ });
214
+
215
+ test("confirm-Antwort ohne handover → loggedInHref-Function bekommt Args ohne handover-Key", async () => {
216
+ globalThis.fetch = mock(
217
+ async () =>
218
+ new Response(
219
+ JSON.stringify({
220
+ user: { id: "u1", tenantId: "t1", roles: ["User"] },
221
+ tenantKey: "acme",
222
+ }),
223
+ { status: 200, headers: { "Content-Type": "application/json" } },
224
+ ),
225
+ ) as unknown as typeof fetch;
226
+
227
+ let receivedArgs:
228
+ | { tenantKey: string; roles: readonly string[]; handover?: unknown }
229
+ | undefined;
230
+ renderWithProviders(
231
+ <SignupCompleteScreen
232
+ token="abc-token"
233
+ loggedInHref={(args) => {
234
+ receivedArgs = args;
235
+ return `/${args.tenantKey}/`;
236
+ }}
237
+ />,
238
+ );
239
+ fillPasswords("validpass1", "validpass1");
240
+ fireEvent.click(screen.getByRole("button", { name: "Activate account" }));
241
+
242
+ await waitFor(() => {
243
+ expect(screen.getByRole("link", { name: "Continue" }).getAttribute("href")).toBe("/acme/");
244
+ });
245
+ expect(receivedArgs).toStrictEqual({ tenantKey: "acme", roles: ["User"] });
246
+ });
247
+
182
248
  test("mismatch → client-side error, kein fetch-Call", async () => {
183
249
  const fetchMock = mock(async () => new Response(null, { status: 200 }));
184
250
  globalThis.fetch = fetchMock as unknown as typeof fetch;
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
19
19
  import { type FormEvent, type ReactNode, useState } from "react";
20
- import { confirmSignup } from "./auth-client";
20
+ import { confirmSignup, type SignupConfirmSuccess } from "./auth-client";
21
21
  import { passwordPairIssue, resolveLoggedInHref } from "./auth-form-logic";
22
22
  import { AuthCard, useUrlToken } from "./auth-form-primitives";
23
23
 
@@ -25,11 +25,16 @@ export type SignupCompleteScreenProps = {
25
25
  readonly title?: string;
26
26
  /** Override for the URL token — server-rendered apps can pass it. Default: `?token=...`. */
27
27
  readonly token?: string;
28
- /** Where to send the user after activation. Function-form receives the tenantKey and the
29
- * roles granted by this flow in that tenant — for role-dependent landing targets. Default "/". */
28
+ /** Where to send the user after activation. Function-form receives the tenantKey, the
29
+ * roles granted by this flow in that tenant, and the claimed `handover` entity when
30
+ * signup claimed a try-first grant. Default "/". */
30
31
  readonly loggedInHref?:
31
32
  | string
32
- | ((args: { tenantKey: string; roles: readonly string[] }) => string);
33
+ | ((args: {
34
+ tenantKey: string;
35
+ roles: readonly string[];
36
+ handover?: SignupConfirmSuccess["handover"];
37
+ }) => string);
33
38
  /** Href for the "already have an account?" link when the token is missing. Default "/login". */
34
39
  readonly loginHref?: string;
35
40
  };
@@ -71,6 +76,7 @@ export function SignupCompleteScreen({
71
76
  resolveLoggedInHref(loggedInHref, {
72
77
  tenantKey: res.data.tenantKey,
73
78
  roles: res.data.user.roles,
79
+ ...(res.data.handover !== undefined && { handover: res.data.handover }),
74
80
  }),
75
81
  );
76
82
  return;
@@ -38,8 +38,9 @@ const crud = createEventStoreExecutor(fileRefsTable, fileRefEntity, { entityName
38
38
  // not the subject axis.
39
39
  // "anonymize": insertedById=null, row + binary survive (FK refs can still
40
40
  // point at it; person-link removed) — applies to ALL rows
41
- // when the entity strategy itself is already anonymize (e.g.
42
- // blockDelete retention), regardless of the field.
41
+ // when a retention override on fileRef (anonymize or
42
+ // blockDelete, e.g. a statutory retention duty) sets the
43
+ // strategy, regardless of the field.
43
44
  //
44
45
  // **Provider-Resolution:** der Provider kommt zur Lauf-Zeit aus
45
46
  // `ctx.buildStorageProvider(ctx.tenantId)` — der Forget-Orchestrator
@@ -216,11 +217,12 @@ export const fileRefDeleteHook: UserDataDeleteHook = async (ctx, strategy) => {
216
217
  });
217
218
 
218
219
  if (strategy !== "delete") {
219
- // anonymize: insertedById=null, FileRef + binary bleiben. Use-case: shared
220
- // chat-Attachment im Multi-User-Channel — Author-ID raus, Datei bleibt sichtbar.
221
- // Applies to ALL rows — the entity strategy comes from a retention
222
- // policy (e.g. blockDelete) and overrides the per-field decision below,
223
- // which only applies for strategy="delete".
220
+ // Only reachable via an explicit retention override on fileRef:
221
+ // policyToStrategy maps anonymize and blockDelete to "anonymize", typically
222
+ // for a statutory retention duty (Art. 17(3)(b) GDPR, HGB/AO). Row and
223
+ // binary must survive, so only the uploader link is severed. Applies to
224
+ // ALL rows because the override replaces the per-field decision below,
225
+ // which only runs for strategy="delete".
224
226
  await severPersonLink(ctx.db, systemUser, rows);
225
227
  // skip: anonymize is complete — the hard-delete path below runs only for strategy "delete".
226
228
  return;
@@ -23,7 +23,12 @@ import {
23
23
  } from "@cosmicdrift/kumiko-framework/engine";
24
24
  import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
25
25
  import { getConsumerState } from "@cosmicdrift/kumiko-framework/pipeline";
26
- import { setupTestStack, type TestStack, TestUsers } from "@cosmicdrift/kumiko-framework/stack";
26
+ import {
27
+ drainEventConsumers,
28
+ setupTestStack,
29
+ type TestStack,
30
+ TestUsers,
31
+ } from "@cosmicdrift/kumiko-framework/stack";
27
32
  import { workflowRunAggregateId } from "../aggregate-id";
28
33
  import { registerEventTrigger } from "../event-trigger";
29
34
  import { workflowRunnerFeature } from "../feature";
@@ -190,7 +195,7 @@ describe("workflow-runner event-trigger", () => {
190
195
  // Consumers run their turns concurrently, so events an earlier test's
191
196
  // workflow emitted may still be ahead of this consumer's cursor; drain
192
197
  // them first so `processed` below counts only this test's trigger.
193
- while (((await stack.eventDispatcher?.runOnce())?.processed ?? 0) > 0) {}
198
+ await drainEventConsumers(stack, [consumerName]);
194
199
 
195
200
  await insertOne(stack.db, eventsTable, {
196
201
  aggregateId: crypto.randomUUID(),