@cosmicdrift/kumiko-bundled-features 0.159.1 → 0.161.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.
Files changed (38) hide show
  1. package/package.json +6 -6
  2. package/src/auth-email-password/__tests__/login-mfa-setup-required.integration.test.ts +129 -0
  3. package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +5 -0
  4. package/src/auth-email-password/handlers/login.write.ts +6 -3
  5. package/src/auth-email-password/i18n.ts +4 -0
  6. package/src/auth-email-password/web/__tests__/auth-client.test.ts +529 -0
  7. package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +57 -0
  8. package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +126 -2
  9. package/src/auth-email-password/web/__tests__/login-screen.test.tsx +134 -0
  10. package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +131 -0
  11. package/src/auth-email-password/web/auth-client.ts +4 -3
  12. package/src/auth-email-password/web/login-screen.tsx +5 -2
  13. package/src/auth-foundation/__tests__/feature.test.ts +16 -3
  14. package/src/auth-foundation/__tests__/session-store.test.ts +1 -0
  15. package/src/auth-foundation/__tests__/tenant-providers.test.ts +136 -0
  16. package/src/auth-foundation/boot-checks.ts +54 -0
  17. package/src/auth-foundation/feature.ts +77 -23
  18. package/src/auth-foundation/index.ts +14 -0
  19. package/src/auth-foundation/resolve-anonymous-access.ts +54 -0
  20. package/src/auth-foundation/types.ts +63 -0
  21. package/src/auth-mfa/__tests__/mfa-preauth-setup-token.test.ts +87 -0
  22. package/src/auth-mfa/__tests__/verify.integration.test.ts +24 -0
  23. package/src/auth-mfa/constants.ts +1 -0
  24. package/src/auth-mfa/handlers/verify.write.ts +1 -1
  25. package/src/auth-mfa/mfa-preauth-setup-token.ts +86 -0
  26. package/src/auth-mfa/mfa-status-checker.ts +13 -4
  27. package/src/config/__tests__/app-overrides.test.ts +11 -0
  28. package/src/config/__tests__/backing-secrets.integration.test.ts +18 -0
  29. package/src/config/__tests__/cascade.integration.test.ts +39 -0
  30. package/src/config/__tests__/pii-encrypted.integration.test.ts +20 -0
  31. package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +154 -0
  32. package/src/inbound-provider-imap/__tests__/plugin-mocked.test.ts +331 -0
  33. package/src/personal-access-tokens/feature.ts +1 -1
  34. package/src/sessions/feature.ts +8 -1
  35. package/src/tags/web/__tests__/tag-manager.test.tsx +277 -8
  36. package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +180 -0
  37. package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +1 -0
  38. package/src/user-profile/__tests__/profile-screen.test.tsx +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.159.1",
3
+ "version": "0.161.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>",
@@ -117,11 +117,11 @@
117
117
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
118
118
  },
119
119
  "dependencies": {
120
- "@cosmicdrift/kumiko-dispatcher-live": "0.159.1",
121
- "@cosmicdrift/kumiko-framework": "0.159.1",
122
- "@cosmicdrift/kumiko-headless": "0.159.1",
123
- "@cosmicdrift/kumiko-renderer": "0.159.1",
124
- "@cosmicdrift/kumiko-renderer-web": "0.159.1",
120
+ "@cosmicdrift/kumiko-dispatcher-live": "0.161.0",
121
+ "@cosmicdrift/kumiko-framework": "0.161.0",
122
+ "@cosmicdrift/kumiko-headless": "0.161.0",
123
+ "@cosmicdrift/kumiko-renderer": "0.161.0",
124
+ "@cosmicdrift/kumiko-renderer-web": "0.161.0",
125
125
  "@mollie/api-client": "^4.5.0",
126
126
  "imapflow": "^1.3.3",
127
127
  "mailparser": "^3.9.8",
@@ -0,0 +1,129 @@
1
+ // #1455: enforcement policy blocks an unenrolled user at login
2
+ // (mfa-setup-required) — the response must carry a preauthSetupToken so a
3
+ // later pre-auth enroll step (#1231) can look the user back up without a
4
+ // session. Real HTTP through setupTestStack, both auth-mfa and
5
+ // auth-email-password mounted together (mirrors auth.integration.test.ts).
6
+
7
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
8
+ import { randomBytes } from "node:crypto";
9
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
10
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
11
+ import {
12
+ createTestUser,
13
+ setupTestStack,
14
+ type TestStack,
15
+ testTenantId,
16
+ unsafeCreateEntityTable,
17
+ unsafePushTables,
18
+ } from "@cosmicdrift/kumiko-framework/stack";
19
+ import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing";
20
+ import {
21
+ createAuthMfaFeature,
22
+ mfaRequiredConfigHandle,
23
+ mfaStatusCheckerFromFeature,
24
+ } from "../../auth-mfa";
25
+ import { userMfaEntity } from "../../auth-mfa/schema/user-mfa";
26
+ import { ConfigHandlers, createConfigFeature } from "../../config";
27
+ import { createConfigResolver } from "../../config/resolver";
28
+ import { configValuesTable } from "../../config/table";
29
+ import { hashPassword } from "../../shared";
30
+ import { createTenantFeature } from "../../tenant";
31
+ import { tenantMembershipsTable } from "../../tenant/membership-table";
32
+ import { tenantEntity } from "../../tenant/schema/tenant";
33
+ import { seedTenantMembership } from "../../tenant/testing";
34
+ import { UserHandlers } from "../../user";
35
+ import { createUserFeature } from "../../user/feature";
36
+ import { userEntity, userTable } from "../../user/schema/user";
37
+ import { AuthErrors, AuthHandlers } from "../constants";
38
+ import { createAuthEmailPasswordFeature } from "../feature";
39
+
40
+ let stack: TestStack;
41
+
42
+ const CHALLENGE_TOKEN_SECRET = "test-mfa-challenge-secret-at-least-32-bytes!!";
43
+ const TENANT_ID: TenantId = testTenantId(400);
44
+
45
+ beforeAll(async () => {
46
+ const encryption = createTestEnvelopeCipher(randomBytes(32).toString("base64"));
47
+ const resolver = createConfigResolver({ cipher: encryption });
48
+ const authMfaFeature = createAuthMfaFeature({
49
+ setupTokenSecret: "test-mfa-setup-token-secret-at-least-32-bytes!!",
50
+ issuer: "Kumiko Test",
51
+ challengeTokenSecret: CHALLENGE_TOKEN_SECRET,
52
+ });
53
+
54
+ stack = await setupTestStack({
55
+ features: [
56
+ createConfigFeature(),
57
+ createUserFeature(),
58
+ createTenantFeature(),
59
+ authMfaFeature,
60
+ createAuthEmailPasswordFeature({
61
+ mfaStatusChecker: mfaStatusCheckerFromFeature(authMfaFeature),
62
+ }),
63
+ ],
64
+ extraContext: { configResolver: resolver, configEncryption: encryption },
65
+ authConfig: {
66
+ membershipQuery: "tenant:query:memberships",
67
+ loginHandler: AuthHandlers.login,
68
+ loginErrorStatusMap: {
69
+ [AuthErrors.invalidCredentials]: 401,
70
+ [AuthErrors.noMembership]: 403,
71
+ },
72
+ },
73
+ });
74
+
75
+ await unsafeCreateEntityTable(stack.db, userEntity);
76
+ await unsafeCreateEntityTable(stack.db, tenantEntity);
77
+ await unsafeCreateEntityTable(stack.db, userMfaEntity);
78
+ await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
79
+ });
80
+
81
+ afterAll(async () => {
82
+ await stack.cleanup();
83
+ });
84
+
85
+ beforeEach(async () => {
86
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
87
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
88
+ });
89
+
90
+ describe("login: mfa-setup-required carries a verifiable preauthSetupToken", () => {
91
+ test("unenrolled user blocked by 'all' policy gets a token instead of a session", async () => {
92
+ const password = "correct-horse-battery-2026";
93
+ const hash = await hashPassword(password);
94
+ const created = await stack.http.writeOk<{ id: string }>(
95
+ UserHandlers.create,
96
+ { email: "unenrolled@example.com", passwordHash: hash, displayName: "Unenrolled" },
97
+ createTestUser({ id: 401, tenantId: TENANT_ID, roles: ["SystemAdmin"] }),
98
+ );
99
+ await seedTenantMembership(stack.db, {
100
+ userId: created.id,
101
+ tenantId: TENANT_ID,
102
+ roles: ["User"],
103
+ });
104
+ await stack.http.writeOk(
105
+ ConfigHandlers.set,
106
+ { key: mfaRequiredConfigHandle.name, value: "all" },
107
+ createTestUser({ id: 402, tenantId: TENANT_ID, roles: ["Admin"] }),
108
+ );
109
+
110
+ const res = await stack.http.raw("POST", "/api/auth/login", {
111
+ email: "unenrolled@example.com",
112
+ password,
113
+ });
114
+
115
+ expect(res.status).toBe(200);
116
+ const body = await res.json();
117
+ expect(body.isSuccess).toBe(true);
118
+ expect(body.mfaSetupRequired).toBe(true);
119
+ expect(body.token).toBeUndefined();
120
+ expect(typeof body.preauthSetupToken).toBe("string");
121
+
122
+ const { verifyMfaPreauthSetupToken } = await import("../../auth-mfa/mfa-preauth-setup-token");
123
+ const verified = verifyMfaPreauthSetupToken(body.preauthSetupToken, CHALLENGE_TOKEN_SECRET);
124
+ expect(verified.ok).toBe(true);
125
+ if (verified.ok) {
126
+ expect(verified.payload).toEqual({ userId: created.id, tenantId: TENANT_ID });
127
+ }
128
+ });
129
+ });
@@ -158,6 +158,11 @@ async function seedUser(opts: {
158
158
  roles: t.roles,
159
159
  });
160
160
  }
161
+ // request-helper mints a sid for systemAdmin when sessionCreator is wired
162
+ // (#1372) — that is seed plumbing, not the auth-route behavior under test.
163
+ store.live.clear();
164
+ store.created.length = 0;
165
+ store.revoked.length = 0;
161
166
  return { userId: created.id };
162
167
  }
163
168
 
@@ -58,7 +58,7 @@ export type LoginHandlerOptions = {
58
58
  ) => Promise<
59
59
  | { readonly required: false }
60
60
  | { readonly required: true; readonly challengeToken: string }
61
- | { readonly setupRequired: true }
61
+ | { readonly setupRequired: true; readonly preauthSetupToken: string }
62
62
  >;
63
63
  };
64
64
 
@@ -70,7 +70,7 @@ const SYSTEM_USER_ID = "00000000-0000-4000-8000-000000000000";
70
70
  type LoginResult =
71
71
  | { readonly kind: "auth-session"; readonly session: SessionUser }
72
72
  | { readonly kind: "mfa-challenge"; readonly challengeToken: string }
73
- | { readonly kind: "mfa-setup-required" };
73
+ | { readonly kind: "mfa-setup-required"; readonly preauthSetupToken: string };
74
74
 
75
75
  // Login — unauthenticated entry point. The route is wired public (no JWT
76
76
  // middleware), synthesising a guest SessionUser for the handler's access
@@ -226,7 +226,10 @@ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
226
226
  };
227
227
  }
228
228
  if ("setupRequired" in mfaStatus) {
229
- return { isSuccess: true, data: { kind: "mfa-setup-required" } };
229
+ return {
230
+ isSuccess: true,
231
+ data: { kind: "mfa-setup-required", preauthSetupToken: mfaStatus.preauthSetupToken },
232
+ };
230
233
  }
231
234
  }
232
235
 
@@ -35,6 +35,8 @@ export const defaultTranslations: TranslationsByLocale = {
35
35
  "auth.errors.loginFailed": "Login fehlgeschlagen.",
36
36
  "auth.errors.mfaNotSupported":
37
37
  "Diese App unterstützt keine Zwei-Faktor-Bestätigung. Bitte an den Support wenden.",
38
+ "auth.errors.mfaSetupRequired":
39
+ "Zwei-Faktor-Authentifizierung erforderlich. Bitte an den Administrator wenden.",
38
40
  "auth.errors.invalidResetToken":
39
41
  "Der Link ist ungültig oder abgelaufen. Bitte fordere einen neuen an.",
40
42
  "auth.errors.invalidVerificationToken": "Der Bestätigungs-Link ist ungültig oder abgelaufen.",
@@ -161,6 +163,8 @@ export const defaultTranslations: TranslationsByLocale = {
161
163
  "auth.errors.loginFailed": "Login failed.",
162
164
  "auth.errors.mfaNotSupported":
163
165
  "This app doesn't support two-factor verification. Please contact support.",
166
+ "auth.errors.mfaSetupRequired":
167
+ "Two-factor authentication required. Please contact your administrator.",
164
168
  "auth.errors.invalidResetToken": "Link is invalid or expired. Please request a new one.",
165
169
  "auth.errors.invalidVerificationToken": "Verification link is invalid or expired.",
166
170
  "auth.errors.invalidUnlockToken":