@cosmicdrift/kumiko-bundled-features 0.165.2 → 0.165.4
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 +7 -7
- package/src/__tests__/access-invalidation-consumer.integration.test.ts +1 -1
- package/src/__tests__/access-invalidation-stream.integration.test.ts +80 -66
- package/src/admin-shell/changes.json +8 -0
- package/src/agent-tools/changes.json +1 -0
- package/src/audit/__tests__/audit.integration.test.ts +5 -2
- package/src/audit/changes.json +8 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +3 -0
- package/src/auth-email-password/changes.json +26 -0
- package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +3 -11
- package/src/auth-email-password/web/__tests__/fetch-mock.ts +14 -0
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +3 -11
- package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +3 -11
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +3 -11
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +3 -11
- package/src/auth-foundation/changes.json +8 -0
- package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +6 -6
- package/src/auth-mfa/changes.json +38 -0
- package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +3 -11
- package/src/auth-mfa-user-data/changes.json +1 -0
- package/src/billing-foundation/changes.json +1 -0
- package/src/cap-counter/changes.json +1 -0
- package/src/channel-email/changes.json +1 -0
- package/src/channel-in-app/changes.json +1 -0
- package/src/channel-push/changes.json +1 -0
- package/src/compliance-profiles/changes.json +1 -0
- package/src/config/changes.json +9 -0
- package/src/crypto-shredding/changes.json +14 -0
- package/src/crypto-shredding/handlers/forget-subject.write.ts +13 -0
- package/src/custom-fields/changes.json +8 -0
- package/src/data-retention/changes.json +8 -0
- package/src/delivery/changes.json +8 -0
- package/src/document-ingest-foundation/changes.json +1 -0
- package/src/feature-toggles/changes.json +26 -0
- package/src/file-foundation/changes.json +1 -0
- package/src/file-provider-inmemory/changes.json +1 -0
- package/src/file-provider-s3/changes.json +1 -0
- package/src/file-provider-s3-env/changes.json +1 -0
- package/src/files/changes.json +1 -0
- package/src/files-provider-s3/changes.json +1 -0
- package/src/folders/changes.json +1 -0
- package/src/folders-user-data/changes.json +1 -0
- package/src/foundation-shared/changes.json +1 -0
- package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +3 -1
- package/src/inbound-mail-foundation/changes.json +8 -0
- package/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
- package/src/inbound-provider-imap/changes.json +1 -0
- package/src/inbound-provider-inmemory/changes.json +1 -0
- package/src/jobs/__tests__/jobs-catalog.integration.test.ts +159 -0
- package/src/jobs/__tests__/jobs-screens.boot.test.ts +1 -0
- package/src/jobs/__tests__/jobs-security.integration.test.ts +4 -2
- package/src/jobs/changes.json +8 -0
- package/src/jobs/constants.ts +2 -0
- package/src/jobs/feature.ts +3 -1
- package/src/jobs/handlers/catalog.query.ts +43 -0
- package/src/jobs/handlers/trigger.write.ts +26 -2
- package/src/jobs/i18n.ts +33 -0
- package/src/jobs/web/__tests__/job-runs-screen.test.tsx +223 -0
- package/src/jobs/web/job-runs-screen.tsx +218 -84
- package/src/ledger/changes.json +1 -0
- package/src/legal-pages/changes.json +1 -0
- package/src/mail-foundation/changes.json +1 -0
- package/src/mail-transport-inmemory/changes.json +1 -0
- package/src/mail-transport-smtp/changes.json +1 -0
- package/src/managed-pages/changes.json +20 -0
- package/src/page-render/changes.json +14 -0
- package/src/personal-access-tokens/changes.json +8 -0
- package/src/presets/changes.json +1 -0
- package/src/presets/dsgvo-self-service.ts +1 -1
- package/src/rate-limiting/changes.json +1 -0
- package/src/readiness/changes.json +1 -0
- package/src/renderer-foundation/changes.json +1 -0
- package/src/renderer-simple/changes.json +1 -0
- package/src/secrets/changes.json +1 -0
- package/src/seo/changes.json +20 -0
- package/src/sessions/changes.json +8 -0
- package/src/sessions/handlers/revoke-all-for-user.write.ts +9 -1
- package/src/shared/changes.json +1 -0
- package/src/step-dispatcher/changes.json +1 -0
- package/src/subscription-mollie/changes.json +1 -0
- package/src/subscription-stripe/changes.json +1 -0
- package/src/tags/changes.json +1 -0
- package/src/template-resolver/changes.json +1 -0
- package/src/tenant/changes.json +27 -0
- package/src/tenant-lifecycle/changes.json +1 -0
- package/src/tenant-settings/changes.json +1 -0
- package/src/text-content/changes.json +14 -0
- package/src/tier-engine/changes.json +1 -0
- package/src/user/changes.json +8 -0
- package/src/user/schema/user.ts +8 -5
- package/src/user-data-rights/__tests__/audit-log.integration.test.ts +7 -3
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/download.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/request-export.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +0 -2
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +0 -1
- package/src/user-data-rights/changes.json +27 -0
- package/src/user-data-rights/feature.ts +7 -6
- package/src/user-data-rights/handlers/lift-restriction.write.ts +4 -20
- package/src/user-data-rights/handlers/restrict-account.write.ts +4 -20
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +1 -0
- package/src/user-data-rights/lib/deny-if-target-outside-admin-tenant.ts +32 -0
- package/src/user-data-rights/run-forget-cleanup.ts +23 -0
- package/src/user-data-rights-defaults/changes.json +8 -0
- package/src/user-profile/changes.json +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.165.
|
|
3
|
+
"version": "0.165.4",
|
|
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>",
|
|
@@ -119,11 +119,11 @@
|
|
|
119
119
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
120
120
|
},
|
|
121
121
|
"dependencies": {
|
|
122
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.165.
|
|
123
|
-
"@cosmicdrift/kumiko-framework": "0.165.
|
|
124
|
-
"@cosmicdrift/kumiko-headless": "0.165.
|
|
125
|
-
"@cosmicdrift/kumiko-renderer": "0.165.
|
|
126
|
-
"@cosmicdrift/kumiko-renderer-web": "0.165.
|
|
122
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.165.4",
|
|
123
|
+
"@cosmicdrift/kumiko-framework": "0.165.4",
|
|
124
|
+
"@cosmicdrift/kumiko-headless": "0.165.4",
|
|
125
|
+
"@cosmicdrift/kumiko-renderer": "0.165.4",
|
|
126
|
+
"@cosmicdrift/kumiko-renderer-web": "0.165.4",
|
|
127
127
|
"@mollie/api-client": "^4.5.0",
|
|
128
128
|
"imapflow": "^1.3.3",
|
|
129
129
|
"mailparser": "^3.9.8",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"LICENSE"
|
|
149
149
|
],
|
|
150
150
|
"peerDependencies": {
|
|
151
|
-
"@cosmicdrift/kumiko-types": "^0.165.
|
|
151
|
+
"@cosmicdrift/kumiko-types": "^0.165.4"
|
|
152
152
|
},
|
|
153
153
|
"devDependencies": {
|
|
154
154
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -72,7 +72,7 @@ let invalidated: string[];
|
|
|
72
72
|
let unsubscribe: (() => void) | undefined;
|
|
73
73
|
|
|
74
74
|
function trackInvalidation(userId: string): void {
|
|
75
|
-
unsubscribe = stack.sseBroker.subscribeAccessInvalidation(userId, () => {
|
|
75
|
+
unsubscribe = stack.sseBroker.subscribeAccessInvalidation?.(userId, () => {
|
|
76
76
|
invalidated.push(userId);
|
|
77
77
|
});
|
|
78
78
|
}
|
|
@@ -53,25 +53,28 @@ const callbacks = createLateBoundHolder<SessionCallbacks>("session-callbacks");
|
|
|
53
53
|
const encryptionKey = randomBytes(32).toString("base64");
|
|
54
54
|
const TENANT = testTenantId(1);
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
releaseNextChunk = resolve;
|
|
56
|
+
// Per-stream gate (gateId in payload) so parallel/second streams don't share
|
|
57
|
+
// one module-level Promise — Map keyed by the id the test picks.
|
|
58
|
+
const chunkGates = new Map<string, { promise: Promise<void>; release: () => void }>();
|
|
59
|
+
|
|
60
|
+
function createGate(gateId: string): () => void {
|
|
61
|
+
let release!: () => void;
|
|
62
|
+
const promise = new Promise<void>((resolve) => {
|
|
63
|
+
release = resolve;
|
|
65
64
|
});
|
|
65
|
+
chunkGates.set(gateId, { promise, release });
|
|
66
|
+
return release;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
const streamProbeFeature = defineFeature("stream-probe", (r) => {
|
|
69
70
|
r.streamHandler(
|
|
70
71
|
"probe:tail",
|
|
71
|
-
z.object({
|
|
72
|
-
async function* () {
|
|
72
|
+
z.object({ gateId: z.string().min(1) }),
|
|
73
|
+
async function* (event) {
|
|
73
74
|
yield { phase: "open" as const };
|
|
74
|
-
|
|
75
|
+
const gate = chunkGates.get(event.payload.gateId);
|
|
76
|
+
if (!gate) throw new Error(`missing gate ${event.payload.gateId}`);
|
|
77
|
+
await gate.promise;
|
|
75
78
|
// Reached only if mid-stream access check failed to cut the generator.
|
|
76
79
|
yield { phase: "should-not-reach-client" as const };
|
|
77
80
|
},
|
|
@@ -144,62 +147,73 @@ beforeEach(async () => {
|
|
|
144
147
|
userSessionTable,
|
|
145
148
|
eventsTable,
|
|
146
149
|
]);
|
|
147
|
-
|
|
150
|
+
chunkGates.clear();
|
|
148
151
|
});
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
153
|
+
async function openProbeStream(token: string): Promise<{
|
|
154
|
+
iter: AsyncIterator<{ phase: string }>;
|
|
155
|
+
release: () => void;
|
|
156
|
+
}> {
|
|
157
|
+
const gateId = generateToken();
|
|
158
|
+
const release = createGate(gateId);
|
|
159
|
+
const { fetch, csrfToken } = buildLiveFetch(token);
|
|
160
|
+
const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => csrfToken });
|
|
161
|
+
const iter = dispatcher
|
|
162
|
+
.stream<{ phase: string }>("stream-probe:stream:probe:tail", { gateId })
|
|
163
|
+
[Symbol.asyncIterator]();
|
|
164
|
+
const first = await iter.next();
|
|
165
|
+
expect(first.done).toBe(false);
|
|
166
|
+
expect(first.value).toEqual({ phase: "open" });
|
|
167
|
+
return { iter, release };
|
|
168
|
+
}
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
describe("access-invalidation mid-stream SSE teardown (#1561)", () => {
|
|
171
|
+
const cases = [
|
|
172
|
+
{
|
|
173
|
+
name: "session revoke mid-stream terminates the open HTTP SSE stream",
|
|
174
|
+
setup: async () => {
|
|
175
|
+
await h.seedUser("stream-revoke@example.com", "pw-long-enough");
|
|
176
|
+
const { token, sid } = await h.login("stream-revoke@example.com", "pw-long-enough");
|
|
177
|
+
return {
|
|
178
|
+
token,
|
|
179
|
+
revoke: async () => {
|
|
180
|
+
const revokeRes = await h.authedPost("/api/write", token, {
|
|
181
|
+
type: SessionHandlers.revoke,
|
|
182
|
+
payload: { id: sid },
|
|
183
|
+
});
|
|
184
|
+
expect(revokeRes.status).toBe(200);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "tenant role strip mid-stream terminates the open HTTP SSE stream",
|
|
191
|
+
setup: async () => {
|
|
192
|
+
const { userId } = await h.seedUser("stream-roles@example.com", "pw-long-enough");
|
|
193
|
+
const { token } = await h.login("stream-roles@example.com", "pw-long-enough");
|
|
194
|
+
return {
|
|
195
|
+
token,
|
|
196
|
+
revoke: async () => {
|
|
197
|
+
const admin = await withMintedSession(sessionCreator, TestUsers.systemAdmin);
|
|
198
|
+
await stack.http.writeOk(
|
|
199
|
+
TenantHandlers.updateMemberRoles,
|
|
200
|
+
{ userId, tenantId: TENANT, roles: ["Admin"] },
|
|
201
|
+
admin,
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
] as const;
|
|
208
|
+
|
|
209
|
+
for (const c of cases) {
|
|
210
|
+
test(c.name, async () => {
|
|
211
|
+
const { token, revoke } = await c.setup();
|
|
212
|
+
const { iter, release } = await openProbeStream(token);
|
|
213
|
+
await revoke();
|
|
214
|
+
await stack.eventDispatcher?.runOnce();
|
|
215
|
+
release();
|
|
216
|
+
await expect(iter.next()).rejects.toMatchObject({ code: "access_denied" });
|
|
169
217
|
});
|
|
170
|
-
|
|
171
|
-
await stack.eventDispatcher?.runOnce();
|
|
172
|
-
|
|
173
|
-
releaseNextChunk?.();
|
|
174
|
-
|
|
175
|
-
await expect(iter.next()).rejects.toMatchObject({ code: "access_denied" });
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("tenant role strip mid-stream terminates the open HTTP SSE stream", async () => {
|
|
179
|
-
const { userId } = await h.seedUser("stream-roles@example.com", "pw-long-enough");
|
|
180
|
-
const { token } = await h.login("stream-roles@example.com", "pw-long-enough");
|
|
181
|
-
|
|
182
|
-
const { fetch, csrfToken } = buildLiveFetch(token);
|
|
183
|
-
const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => csrfToken });
|
|
184
|
-
|
|
185
|
-
const iter = dispatcher
|
|
186
|
-
.stream<{ phase: string }>("stream-probe:stream:probe:tail", {})
|
|
187
|
-
[Symbol.asyncIterator]();
|
|
188
|
-
|
|
189
|
-
const first = await iter.next();
|
|
190
|
-
expect(first.done).toBe(false);
|
|
191
|
-
expect(first.value).toEqual({ phase: "open" });
|
|
192
|
-
|
|
193
|
-
const admin = await withMintedSession(sessionCreator, TestUsers.systemAdmin);
|
|
194
|
-
await stack.http.writeOk(
|
|
195
|
-
TenantHandlers.updateMemberRoles,
|
|
196
|
-
{ userId, tenantId: TENANT, roles: ["Admin"] },
|
|
197
|
-
admin,
|
|
198
|
-
);
|
|
199
|
-
await stack.eventDispatcher?.runOnce();
|
|
200
|
-
|
|
201
|
-
releaseNextChunk?.();
|
|
202
|
-
|
|
203
|
-
await expect(iter.next()).rejects.toMatchObject({ code: "access_denied" });
|
|
204
|
-
});
|
|
218
|
+
}
|
|
205
219
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.125.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "admin-shell: neues bundled-feature für rollen-gated Tenant- und Platform-Workspaces mit O…",
|
|
6
|
+
"detail": "admin-shell: neues bundled-feature für rollen-gated Tenant- und Platform-Workspaces mit Overview-Home-Screens, Nav-Icons und Server-i18n. Komponiert Screens aus `tenant`, `audit`, `jobs` und optional `tier-engine` — mount nach diesen Features. Overview-Queries laufen über eine fest kodierte Allowlist pro Workspace, um versehentliche Cross-Workspace-Datenzugriffe zu verhindern."
|
|
7
|
+
}
|
|
8
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -212,7 +212,7 @@ describe("audit: list query", () => {
|
|
|
212
212
|
];
|
|
213
213
|
for (const [name, createdAt] of stamps) {
|
|
214
214
|
const id = byName.get(name);
|
|
215
|
-
|
|
215
|
+
if (!id) throw new Error(`missing event for ${name}`);
|
|
216
216
|
await asRawClient(stack.db).unsafe(
|
|
217
217
|
`UPDATE kumiko_events SET created_at = $1::timestamptz WHERE id = $2::bigint`,
|
|
218
218
|
[createdAt, id],
|
|
@@ -244,7 +244,10 @@ describe("audit: list query", () => {
|
|
|
244
244
|
admin,
|
|
245
245
|
);
|
|
246
246
|
expect(untilBefore.rows).toHaveLength(1);
|
|
247
|
-
|
|
247
|
+
const untilRow = untilBefore.rows[0];
|
|
248
|
+
expect(untilRow).toBeDefined();
|
|
249
|
+
if (!untilRow) throw new Error("expected untilBefore row");
|
|
250
|
+
expect((untilRow.payload as { name?: string }).name).toBe("before-window");
|
|
248
251
|
});
|
|
249
252
|
|
|
250
253
|
test("rejects inverted from/to range with validation_error", async () => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.127.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Harden admin operator UI: stricter boot i18n/entityList validation, job run logger wiring…",
|
|
6
|
+
"detail": "Harden admin operator UI: stricter boot i18n/entityList validation, job run logger wiring, audit/job filters, shell breadcrumbs, and bundled entityList/i18n standards."
|
|
7
|
+
}
|
|
8
|
+
]
|
|
@@ -133,6 +133,9 @@ beforeAll(async () => {
|
|
|
133
133
|
password: "warmup-password-not-asserted-on",
|
|
134
134
|
tenants: [],
|
|
135
135
|
});
|
|
136
|
+
// Pins the authHeader sid-cache key: one create for the seed actor, then
|
|
137
|
+
// we reset. If the cache key drifts, this fails instead of leaking into tests.
|
|
138
|
+
expect(store.created).toHaveLength(1);
|
|
136
139
|
store.live.clear();
|
|
137
140
|
store.created.length = 0;
|
|
138
141
|
store.revoked.length = 0;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.162.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "auth-email-password: runtime toggle for self-registration.",
|
|
6
|
+
"detail": "auth-email-password: runtime toggle for self-registration. A new handler-less companion feature (`auth-self-registration`, toggleable, default on) lets an operator flip self-signup off at runtime via feature-toggles without redeploying. `signup-request` silently no-ops when off (matching its always-200 anti-enumeration contract), and a new anonymous-readable query (`auth-email-password:query:signup-registration-status`) lets the public signup page hide its own link/form."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.154.1",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Export `LoginScreen`/`LoginScreenProps`/`AuthLegalLink` from `@cosmicdrift/kumiko-bundled…",
|
|
12
|
+
"detail": "Export `LoginScreen`/`LoginScreenProps`/`AuthLegalLink` from `@cosmicdrift/kumiko-bundled-features/auth-email-password/web`. Every other auth screen (ForgotPasswordScreen, SignupScreen, ResetPasswordScreen, …) was already exported from the barrel with its props type; `LoginScreen` was missed. `makeAuthGate`'s second parameter is typed `LoginScreenProps`, so consumers passing a typed `loginScreenProps` override (e.g. `@cosmicdriftgamestudio/kumiko-designer`) couldn't import the type at all. - @cosmicdrift/kumiko-framework@0.154.1 - @cosmicdrift/kumiko-dispatcher-live@0.154.1 - @cosmicdrift/kumiko-headless@0.154.1 - @cosmicdrift/kumiko-renderer@0.154.1 - @cosmicdrift/kumiko-renderer-web@0.154.1"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.151.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "`auth-email-password/web` no longer exports `LoginScreen`/`LoginScreenProps` directly — u…",
|
|
18
|
+
"detail": "`auth-email-password/web` no longer exports `LoginScreen`/`LoginScreenProps` directly — use the new `createLoginRoute({ loginScreenProps, mfaVerifyScreen, unaffected (they already build on the same logic internally now). Why: a raw `<LoginScreen />` render has no MFA-challenge handling unless the caller remembers to hand-wire `onMfaChallenge` + swap in a verify screen itself — exactly how kumiko-framework#266's login-time MFA step went missing in a real app's standalone apex/marketing login route (it renders outside `emailPasswordClient`'s own gate, which already handled this correctly). `createLoginRoute` is the one place this logic lives now, for both the gated and standalone cases — there's no lower-level piece left to misuse. The `apex-surface-auth` recipe is updated to match."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.131.0",
|
|
22
|
+
"type": "improvement",
|
|
23
|
+
"title": "App-Mounting 2.0 Säule A: Mid-Level-Widget-Kit in renderer-web (StatCard, MiniStat, Secti…",
|
|
24
|
+
"detail": "App-Mounting 2.0 Säule A: Mid-Level-Widget-Kit in renderer-web (StatCard, MiniStat, SectionCard, StatusBadge, ProgressBar, CollapsibleSection, DetailList, ModeSwitch, StatusBarChart, TimeseriesChart, EmptyState/LoadingState/ErrorState, QueryTable) + Status-Farb-Tokens (--color-status-\\*). Neue Hooks useMutation + useDisclosure. Neues Core-Primitive Link (default/button/muted), Button-Variant \"link\", Text-Variant \"muted\"; auth-email-password nutzt sie (authButtonClass/authMutedLinkClass entfernt)."
|
|
25
|
+
}
|
|
26
|
+
]
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { ConfirmAccountUnlockScreen } from "../confirm-account-unlock-screen";
|
|
4
|
+
import { installFetchMock } from "./fetch-mock";
|
|
4
5
|
import { renderWithProviders } from "./test-utils";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
globalThis.fetch = mock(
|
|
10
|
-
async () => new Response(null, { status: 200 }),
|
|
11
|
-
) as unknown as typeof fetch;
|
|
12
|
-
});
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
globalThis.fetch = originalFetch;
|
|
15
|
-
});
|
|
7
|
+
installFetchMock();
|
|
16
8
|
|
|
17
9
|
describe("ConfirmAccountUnlockScreen", () => {
|
|
18
10
|
test("ohne Token → missing-token page", () => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { afterEach, beforeEach, mock } from "bun:test";
|
|
2
|
+
|
|
3
|
+
/** Install a default 200-OK fetch mock; restores the pristine global after each test. */
|
|
4
|
+
export function installFetchMock(): void {
|
|
5
|
+
const pristine = globalThis.fetch;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
globalThis.fetch = mock(
|
|
8
|
+
async () => new Response(null, { status: 200 }),
|
|
9
|
+
) as unknown as typeof fetch;
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
globalThis.fetch = pristine;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { ForgotPasswordScreen } from "../forgot-password-screen";
|
|
4
|
+
import { installFetchMock } from "./fetch-mock";
|
|
4
5
|
import { renderWithProviders } from "./test-utils";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
globalThis.fetch = mock(
|
|
10
|
-
async () => new Response(null, { status: 200 }),
|
|
11
|
-
) as unknown as typeof fetch;
|
|
12
|
-
});
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
globalThis.fetch = originalFetch;
|
|
15
|
-
});
|
|
7
|
+
installFetchMock();
|
|
16
8
|
|
|
17
9
|
describe("ForgotPasswordScreen", () => {
|
|
18
10
|
test("rendert title + email-input + submit-button (de)", () => {
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { RequestAccountUnlockScreen } from "../request-account-unlock-screen";
|
|
4
|
+
import { installFetchMock } from "./fetch-mock";
|
|
4
5
|
import { renderWithProviders } from "./test-utils";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
globalThis.fetch = mock(
|
|
10
|
-
async () => new Response(null, { status: 200 }),
|
|
11
|
-
) as unknown as typeof fetch;
|
|
12
|
-
});
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
globalThis.fetch = originalFetch;
|
|
15
|
-
});
|
|
7
|
+
installFetchMock();
|
|
16
8
|
|
|
17
9
|
describe("RequestAccountUnlockScreen", () => {
|
|
18
10
|
test("renders title + email input + submit (de)", () => {
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { ResetPasswordScreen } from "../reset-password-screen";
|
|
4
|
+
import { installFetchMock } from "./fetch-mock";
|
|
4
5
|
import { renderWithProviders } from "./test-utils";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
globalThis.fetch = mock(
|
|
10
|
-
async () => new Response(null, { status: 200 }),
|
|
11
|
-
) as unknown as typeof fetch;
|
|
12
|
-
});
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
globalThis.fetch = originalFetch;
|
|
15
|
-
});
|
|
7
|
+
installFetchMock();
|
|
16
8
|
|
|
17
9
|
describe("ResetPasswordScreen", () => {
|
|
18
10
|
test("ohne Token in URL UND ohne token-Prop → missing-token-Page", () => {
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { VerifyEmailScreen } from "../verify-email-screen";
|
|
4
|
+
import { installFetchMock } from "./fetch-mock";
|
|
4
5
|
import { renderWithProviders } from "./test-utils";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
globalThis.fetch = mock(
|
|
10
|
-
async () => new Response(null, { status: 200 }),
|
|
11
|
-
) as unknown as typeof fetch;
|
|
12
|
-
});
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
globalThis.fetch = originalFetch;
|
|
15
|
-
});
|
|
7
|
+
installFetchMock();
|
|
16
8
|
|
|
17
9
|
describe("VerifyEmailScreen", () => {
|
|
18
10
|
test("ohne Token → missing-token-Page", () => {
|
|
@@ -92,10 +92,10 @@ type RawUserMfaRow = {
|
|
|
92
92
|
version: number;
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
-
async function readRawRow(userId
|
|
96
|
-
const rows = await selectMany<RawUserMfaRow>(stack.db, userMfaTable,
|
|
95
|
+
async function readRawRow(userId: string): Promise<RawUserMfaRow> {
|
|
96
|
+
const rows = await selectMany<RawUserMfaRow>(stack.db, userMfaTable, { userId });
|
|
97
97
|
const row = rows[0];
|
|
98
|
-
if (!row) throw new Error(
|
|
98
|
+
if (!row) throw new Error(`no user-mfa row for ${userId}`);
|
|
99
99
|
return row;
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -132,7 +132,7 @@ describe("auth-mfa KEK-rotation job — kumiko-framework#266 Step 8", () => {
|
|
|
132
132
|
user,
|
|
133
133
|
);
|
|
134
134
|
|
|
135
|
-
const beforeRow = await readRawRow();
|
|
135
|
+
const beforeRow = await readRawRow(user.id);
|
|
136
136
|
expect(JSON.parse(beforeRow.totpSecret).kekVersion).toBe(1);
|
|
137
137
|
|
|
138
138
|
// Simulate "ops added a new master key version and flipped CURRENT=2" —
|
|
@@ -149,7 +149,7 @@ describe("auth-mfa KEK-rotation job — kumiko-framework#266 Step 8", () => {
|
|
|
149
149
|
|
|
150
150
|
await mfaReencryptJob({}, jobCtx());
|
|
151
151
|
|
|
152
|
-
const afterJobRow = await readRawRow();
|
|
152
|
+
const afterJobRow = await readRawRow(user.id);
|
|
153
153
|
expect(JSON.parse(afterJobRow.totpSecret).kekVersion).toBe(2);
|
|
154
154
|
|
|
155
155
|
// The regression guard: rebuild the projection from scratch (replays
|
|
@@ -161,7 +161,7 @@ describe("auth-mfa KEK-rotation job — kumiko-framework#266 Step 8", () => {
|
|
|
161
161
|
registry: stack.registry,
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
const afterRebuildRow = await readRawRow();
|
|
164
|
+
const afterRebuildRow = await readRawRow(user.id);
|
|
165
165
|
expect(JSON.parse(afterRebuildRow.totpSecret).kekVersion).toBe(2);
|
|
166
166
|
|
|
167
167
|
// Decrypt-level proof, not just the version tag: a valid TOTP code
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.165.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "KEK reencrypt job: rejects unrecognized envelopes",
|
|
6
|
+
"detail": "Reencrypt job now explicitly fails on non-envelope rows instead of silently passing through."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.165.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "MFA: fail-closed when ctx.redis is missing",
|
|
12
|
+
"detail": "Verify/enable-confirm operations now fail when Redis is unreachable instead of silently accepting."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.150.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Fix `auth-mfa`'s two-step verify + enroll flow re-checking stale state and skipping repla…",
|
|
18
|
+
"detail": "Fix `auth-mfa`'s two-step verify + enroll flow re-checking stale state and skipping replay protection that `login.write.ts` already enforces: - `verify.write.ts` minted a session from a challenge token alone, without re-checking account status or tenant membership. A user restricted/deleted, or removed from the tenant, in the ~10-minute window between password login and MFA verify still got a full session — membership loss even silently fell back to global-only roles instead of refusing. Both gates now mirror `login.write.ts`. - `enable-confirm.write.ts` never burned the setup token on success: the same token could re-enable MFA with an old secret after a `disable` (replay), and two parallel confirms both hit the executor instead of the second seeing `mfa_already_enabled`. The token is now single-use, the same as `login.write.ts`'s magic-link tokens. - `auth-mfa` was missing `r.requires(\"tenant\")` despite `verify.write.ts` dispatching `tenant:query:memberships` — a mount without `tenant` now fails at boot instead of 500ing on first login. - Recovery codes are now normalized (uppercased, punctuation-stripped) before hashing and verifying, so a code retyped lowercase or without the dash still matches — this changes the stored hash format, safe because `auth-mfa` has no deployed consumer with enrolled users yet. Also fixes `samples/apps/user-data-rights-demo`'s forget-cleanup test, which asserted on the manual `runForgetCleanup` helper instead of driving the actually-registered cron job through its real `JobContext` wrapper."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.149.1",
|
|
22
|
+
"type": "fix",
|
|
23
|
+
"title": "Fix `auth-mfa` crashing on enable/regenerate for any app with per-subject crypto-shreddin…",
|
|
24
|
+
"detail": "Fix `auth-mfa` crashing on enable/regenerate for any app with per-subject crypto-shredding (`configurePiiSubjectKms`) active: `recoveryCodes` was a `jsonb` field marked `userOwned`, but the PII-encryption pipeline requires a string value for any subject-owned field — every real enable-confirm write threw `PII field \"recoveryCodes\" must be a string, got object`. `recoveryCodes` is now a `createTextField({ encrypted: true, userOwned })` (JSON-stringified at the write/read boundary), matching `totpSecret`. That exposed a second, deeper bug: a field carrying both `encrypted: true` and a subject marker is written as `PII(envelope(plaintext))`, but `decryptForRead` peeled the layers in write order instead of reverse order, so the envelope cipher choked on a still-PII-wrapped string. Fixed `event-store-executor-context.ts`'s `decryptForRead` to unwrap PII first, then envelope — and updated `auth-mfa`'s KEK-reencrypt job (which reads raw rows, bypassing `decryptForRead`) to do the same. Also: `MfaEnableScreen` now takes an optional `onEnabled` callback, fired after a successful enable-confirm — hosts that compose it inside their own MFA-status view (like kumiko-studio's account-security screen) need it to know when to refetch and swap away from the embedded enable flow. - @cosmicdrift/kumiko-framework@0.149.1 - @cosmicdrift/kumiko-headless@0.149.1 - @cosmicdrift/kumiko-renderer@0.149.1 - @cosmicdrift/kumiko-dispatcher-live@0.149.1 - @cosmicdrift/kumiko-renderer-web@0.149.1"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.149.0",
|
|
28
|
+
"type": "improvement",
|
|
29
|
+
"title": "`auth-mfa/web` gains `MfaDisableDialog`, `MfaRegenerateRecoveryDialog`, and `MfaRecoveryC…",
|
|
30
|
+
"detail": "`auth-mfa/web` gains `MfaDisableDialog`, `MfaRegenerateRecoveryDialog`, and `MfaRecoveryCodesReveal` — the disable/regenerate-recovery UI deferred from the initial MFA-UI PR (kumiko-framework#266). Also fixes `MfaEnableScreen`'s error banner, which templated the raw snake_case server error code straight into the i18n key (`auth.mfa.errors.invalid_totp_code`) instead of the camelCase key actually registered (`auth.mfa.errors.invalidCode`) — extracted into a shared `mfaManageErrorKey` helper, now used by all three write-triggering components."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "0.149.0",
|
|
34
|
+
"type": "improvement",
|
|
35
|
+
"title": "`auth-mfa` gains a `status` query (`AuthMfaQueries.status`, wire QN `auth-mfa:query:user-…",
|
|
36
|
+
"detail": "`auth-mfa` gains a `status` query (`AuthMfaQueries.status`, wire QN `auth-mfa:query:user-mfa:status`) returning `{ enabled: boolean }` for the calling user — the one thing a settings/security screen needs to decide whether to show the enrollment flow or the disable/regenerate actions. No client-side signal carried this before."
|
|
37
|
+
}
|
|
38
|
+
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
createStaticLocaleResolver,
|
|
4
4
|
LocaleProvider,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { defaultPrimitives } from "@cosmicdrift/kumiko-renderer-web";
|
|
8
8
|
import { fireEvent, type RenderResult, render, screen, waitFor } from "@testing-library/react";
|
|
9
9
|
import type { ReactElement } from "react";
|
|
10
|
+
import { installFetchMock } from "../../../auth-email-password/web/__tests__/fetch-mock";
|
|
10
11
|
import { makeSessionApi } from "../../../auth-email-password/web/__tests__/test-utils";
|
|
11
12
|
import { SessionContext } from "../../../auth-email-password/web/session";
|
|
12
13
|
import { defaultTranslations } from "../i18n";
|
|
@@ -25,16 +26,7 @@ function renderScreen(ui: ReactElement, session = makeSessionApi()): RenderResul
|
|
|
25
26
|
);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
globalThis.fetch = mock(
|
|
32
|
-
async () => new Response(null, { status: 200 }),
|
|
33
|
-
) as unknown as typeof fetch;
|
|
34
|
-
});
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
globalThis.fetch = originalFetch;
|
|
37
|
-
});
|
|
29
|
+
installFetchMock();
|
|
38
30
|
|
|
39
31
|
describe("MfaVerifyScreen", () => {
|
|
40
32
|
test("rendert Titel + Code-Feld + Submit-Button (de)", () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.165.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "Reencrypt job: removed legacy-decrypt path",
|
|
6
|
+
"detail": "The old single-key format (CONFIG_ENCRYPTION_KEY) is no longer supported. The reencrypt job now classifies rows as rotate/current/unrecognized — unreadable rows are counted as failed instead of silently attempted.",
|
|
7
|
+
"migration": "If any config values still exist in the old single-key format, re-encrypt them with the current envelope format before upgrading. The job will now throw an error instead of attempting migration."
|
|
8
|
+
}
|
|
9
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.165.3",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Searchable subject-PII via derived Meili index (fw#1610).",
|
|
6
|
+
"detail": "Fields may be searchable + userOwned/tenantOwned: the search consumer decrypts into Meili (ciphertext stays at rest). Forget erases the subject key and purgeSearchDocumentsForSubject removes Meili docs (ciphertext LIKE + ownership for post-anonymize). sortable + subject remains forbidden; sensitive + searchable is rejected at boot."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.119.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Crypto-shredding phase D: forget wire.",
|
|
12
|
+
"detail": "Crypto-shredding phase D: forget wire. New `crypto-shredding` bundled feature with the `forget-subject` operator command (DPO/SystemAdmin) — erases a user/tenant subject key and appends a `subject-forgotten` audit event. `user-data-rights` forget-cleanup now erases the user's subject key inside the per-user sub-tx (crash-safe, before the status flip). Fixes `list()` returning ciphertext for camelCase encrypted/pii fields and caching plaintext rows."
|
|
13
|
+
}
|
|
14
|
+
]
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { nullBlindIndexesForSubject } from "@cosmicdrift/kumiko-framework/db";
|
|
9
9
|
import { defineWriteHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
10
10
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
11
|
+
import { purgeSearchDocumentsForSubject } from "@cosmicdrift/kumiko-framework/search";
|
|
11
12
|
import { z } from "zod";
|
|
12
13
|
import { CRYPTO_SHREDDING_AGGREGATE_TYPE, SUBJECT_FORGOTTEN_EVENT_NAME } from "../constants";
|
|
13
14
|
|
|
@@ -75,6 +76,18 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
75
76
|
// adressiert das Subject tenant-übergreifend.
|
|
76
77
|
await nullBlindIndexesForSubject(ctx.db.raw, ctx.registry.features, subjectKey);
|
|
77
78
|
|
|
79
|
+
// Derived search index still holds plaintext (#1610) — purge next to the
|
|
80
|
+
// blind-index sweep. No adapter → no-op (apps without search).
|
|
81
|
+
if (ctx.searchAdapter) {
|
|
82
|
+
await purgeSearchDocumentsForSubject(
|
|
83
|
+
ctx.db.raw,
|
|
84
|
+
ctx.registry.features,
|
|
85
|
+
ctx.searchAdapter,
|
|
86
|
+
subjectKey,
|
|
87
|
+
subject,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
await ctx.unsafeAppendEvent({
|
|
79
92
|
aggregateId: raw.kind === "user" ? raw.userId : raw.tenantId,
|
|
80
93
|
aggregateType: CRYPTO_SHREDDING_AGGREGATE_TYPE,
|