@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 +9 -9
- package/src/auth-email-password/changes.json +6 -0
- package/src/auth-email-password/web/__tests__/signup-complete-screen.test.tsx +66 -0
- package/src/auth-email-password/web/signup-complete-screen.tsx +10 -4
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +9 -7
- package/src/workflow-runner/__tests__/workflow-runner.integration.test.ts +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "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.
|
|
137
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
138
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
139
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
140
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
141
|
-
"@cosmicdrift/kumiko-types": "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.
|
|
171
|
-
"@cosmicdrift/kumiko-locale-es": "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
|
|
29
|
-
* roles granted by this flow in that tenant
|
|
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: {
|
|
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
|
|
42
|
-
// blockDelete retention)
|
|
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
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
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 {
|
|
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
|
-
|
|
198
|
+
await drainEventConsumers(stack, [consumerName]);
|
|
194
199
|
|
|
195
200
|
await insertOne(stack.db, eventsTable, {
|
|
196
201
|
aggregateId: crypto.randomUUID(),
|