@cosmicdrift/kumiko-bundled-features 0.157.0 → 0.157.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-bundled-features",
3
- "version": "0.157.0",
3
+ "version": "0.157.1",
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>",
@@ -116,11 +116,11 @@
116
116
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
117
117
  },
118
118
  "dependencies": {
119
- "@cosmicdrift/kumiko-dispatcher-live": "0.157.0",
120
- "@cosmicdrift/kumiko-framework": "0.157.0",
121
- "@cosmicdrift/kumiko-headless": "0.157.0",
122
- "@cosmicdrift/kumiko-renderer": "0.157.0",
123
- "@cosmicdrift/kumiko-renderer-web": "0.157.0",
119
+ "@cosmicdrift/kumiko-dispatcher-live": "0.157.1",
120
+ "@cosmicdrift/kumiko-framework": "0.157.1",
121
+ "@cosmicdrift/kumiko-headless": "0.157.1",
122
+ "@cosmicdrift/kumiko-renderer": "0.157.1",
123
+ "@cosmicdrift/kumiko-renderer-web": "0.157.1",
124
124
  "@mollie/api-client": "^4.5.0",
125
125
  "imapflow": "^1.3.3",
126
126
  "mailparser": "^3.9.8",
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ invalidChallengeToken,
4
+ invalidRecoveryCode,
5
+ invalidSetupToken,
6
+ invalidTotpCode,
7
+ mfaAlreadyEnabled,
8
+ mfaNotEnabled,
9
+ tooManyAttempts,
10
+ } from "../errors";
11
+ import { defaultTranslations } from "../web/i18n";
12
+
13
+ const failures = {
14
+ mfaAlreadyEnabled: mfaAlreadyEnabled(),
15
+ mfaNotEnabled: mfaNotEnabled(),
16
+ invalidSetupToken: invalidSetupToken(),
17
+ invalidTotpCode: invalidTotpCode(),
18
+ invalidRecoveryCode: invalidRecoveryCode(),
19
+ invalidChallengeToken: invalidChallengeToken(),
20
+ tooManyAttempts: tooManyAttempts(30),
21
+ };
22
+
23
+ describe("auth-mfa errors.ts i18nKey / client-bundle parity", () => {
24
+ for (const [name, failure] of Object.entries(failures)) {
25
+ test(`${name}'s i18nKey is registered in en/de translations`, () => {
26
+ const key = failure.error.i18nKey;
27
+ expect(defaultTranslations["en"]?.[key]).toBeDefined();
28
+ expect(defaultTranslations["de"]?.[key]).toBeDefined();
29
+ });
30
+ }
31
+ });
@@ -19,10 +19,9 @@ export const AuthMfaQueries = {
19
19
  status: "auth-mfa:query:user-mfa:status",
20
20
  } as const;
21
21
 
22
- // Write-failure codes shared between errors.ts (server, mints them into
23
- // UnprocessableError) and web/mfa-error-keys.ts (client, maps them to i18n
24
- // keys) one source so a renamed code can't drift out of sync between the
25
- // two switches.
22
+ // Write-failure codes minted server-side into UnprocessableError (errors.ts).
23
+ // The client reads the i18n key straight off the write result
24
+ // (res.error.i18nKey) instead of re-deriving it from these codes.
26
25
  export const AuthMfaErrorCodes = {
27
26
  mfaAlreadyEnabled: "mfa_already_enabled",
28
27
  mfaNotEnabled: "mfa_not_enabled",
@@ -31,10 +30,6 @@ export const AuthMfaErrorCodes = {
31
30
  invalidRecoveryCode: "invalid_recovery_code",
32
31
  invalidChallengeToken: "invalid_challenge_token",
33
32
  tooManyAttempts: "too_many_attempts",
34
- // Client-only: minted by mfa-enable-screen.tsx on a network/QR-render
35
- // failure during enrollment, never by errors.ts server-side — kept here
36
- // anyway so mfa-error-keys.ts's key-parity test covers it automatically.
37
- setupFailed: "setup_failed",
38
33
  } as const;
39
34
 
40
35
  export const MFA_SETUP_TOKEN_TTL_MINUTES = 10;
@@ -10,7 +10,7 @@ export { AuthMfaErrorCodes as AuthMfaErrors } from "./constants";
10
10
  export function mfaAlreadyEnabled(): WriteFailure {
11
11
  return writeFailure(
12
12
  new UnprocessableError(AuthMfaErrorCodes.mfaAlreadyEnabled, {
13
- i18nKey: "authMfa.errors.mfaAlreadyEnabled",
13
+ i18nKey: "auth.mfa.errors.mfaAlreadyEnabled",
14
14
  }),
15
15
  );
16
16
  }
@@ -18,7 +18,7 @@ export function mfaAlreadyEnabled(): WriteFailure {
18
18
  export function mfaNotEnabled(): WriteFailure {
19
19
  return writeFailure(
20
20
  new UnprocessableError(AuthMfaErrorCodes.mfaNotEnabled, {
21
- i18nKey: "authMfa.errors.mfaNotEnabled",
21
+ i18nKey: "auth.mfa.errors.mfaNotEnabled",
22
22
  }),
23
23
  );
24
24
  }
@@ -26,7 +26,7 @@ export function mfaNotEnabled(): WriteFailure {
26
26
  export function invalidSetupToken(): WriteFailure {
27
27
  return writeFailure(
28
28
  new UnprocessableError(AuthMfaErrorCodes.invalidSetupToken, {
29
- i18nKey: "authMfa.errors.invalidSetupToken",
29
+ i18nKey: "auth.mfa.errors.invalidSetupToken",
30
30
  }),
31
31
  );
32
32
  }
@@ -34,7 +34,7 @@ export function invalidSetupToken(): WriteFailure {
34
34
  export function invalidTotpCode(): WriteFailure {
35
35
  return writeFailure(
36
36
  new UnprocessableError(AuthMfaErrorCodes.invalidTotpCode, {
37
- i18nKey: "authMfa.errors.invalidTotpCode",
37
+ i18nKey: "auth.mfa.errors.invalidTotpCode",
38
38
  }),
39
39
  );
40
40
  }
@@ -42,7 +42,7 @@ export function invalidTotpCode(): WriteFailure {
42
42
  export function invalidRecoveryCode(): WriteFailure {
43
43
  return writeFailure(
44
44
  new UnprocessableError(AuthMfaErrorCodes.invalidRecoveryCode, {
45
- i18nKey: "authMfa.errors.invalidRecoveryCode",
45
+ i18nKey: "auth.mfa.errors.invalidRecoveryCode",
46
46
  }),
47
47
  );
48
48
  }
@@ -50,7 +50,7 @@ export function invalidRecoveryCode(): WriteFailure {
50
50
  export function invalidChallengeToken(): WriteFailure {
51
51
  return writeFailure(
52
52
  new UnprocessableError(AuthMfaErrorCodes.invalidChallengeToken, {
53
- i18nKey: "authMfa.errors.invalidChallengeToken",
53
+ i18nKey: "auth.mfa.errors.invalidChallengeToken",
54
54
  }),
55
55
  );
56
56
  }
@@ -59,7 +59,7 @@ export function invalidChallengeToken(): WriteFailure {
59
59
  export function tooManyAttempts(retryAfterSeconds: number): WriteFailure {
60
60
  return writeFailure(
61
61
  new UnprocessableError(AuthMfaErrorCodes.tooManyAttempts, {
62
- i18nKey: "authMfa.errors.tooManyAttempts",
62
+ i18nKey: "auth.mfa.errors.tooManyAttempts",
63
63
  details: { retryAfterSeconds },
64
64
  }),
65
65
  );
@@ -15,6 +15,9 @@ export const defaultTranslations: TranslationsByLocale = {
15
15
  "auth.mfa.verify.submitting": "…",
16
16
  "auth.mfa.verify.backToLogin": "Zurück zum Login",
17
17
  "auth.mfa.errors.invalidCode": "Ungültiger Code. Bitte erneut versuchen.",
18
+ "auth.mfa.errors.invalidTotpCode": "Ungültiger Code. Bitte erneut versuchen.",
19
+ "auth.mfa.errors.invalidChallengeToken":
20
+ "Die Anmeldung ist abgelaufen. Bitte erneut einloggen.",
18
21
  "auth.mfa.errors.challengeExpired": "Die Anmeldung ist abgelaufen. Bitte erneut einloggen.",
19
22
  "auth.mfa.errors.tooManyAttempts": "Zu viele Fehlversuche. Bitte erneut einloggen.",
20
23
  "auth.mfa.errors.verifyFailed": "Bestätigung fehlgeschlagen.",
@@ -66,6 +69,8 @@ export const defaultTranslations: TranslationsByLocale = {
66
69
  "auth.mfa.verify.submitting": "…",
67
70
  "auth.mfa.verify.backToLogin": "Back to login",
68
71
  "auth.mfa.errors.invalidCode": "Invalid code. Please try again.",
72
+ "auth.mfa.errors.invalidTotpCode": "Invalid code. Please try again.",
73
+ "auth.mfa.errors.invalidChallengeToken": "Your sign-in has expired. Please sign in again.",
69
74
  "auth.mfa.errors.challengeExpired": "Your sign-in has expired. Please sign in again.",
70
75
  "auth.mfa.errors.tooManyAttempts": "Too many failed attempts. Please sign in again.",
71
76
  "auth.mfa.errors.verifyFailed": "Verification failed.",
@@ -13,7 +13,6 @@ export type { MfaDisableDialogProps } from "./mfa-disable-dialog";
13
13
  export { MfaDisableDialog } from "./mfa-disable-dialog";
14
14
  export type { MfaEnableScreenProps } from "./mfa-enable-screen";
15
15
  export { MfaEnableScreen } from "./mfa-enable-screen";
16
- export { mfaManageErrorKey } from "./mfa-error-keys";
17
16
  export type { MfaRecoveryCodesRevealProps } from "./mfa-recovery-codes-reveal";
18
17
  export { MfaRecoveryCodesReveal } from "./mfa-recovery-codes-reveal";
19
18
  export type { MfaRegenerateRecoveryDialogProps } from "./mfa-regenerate-recovery-dialog";
@@ -13,7 +13,6 @@
13
13
  import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
14
14
  import { type ReactNode, useState } from "react";
15
15
  import { AuthMfaHandlers } from "../constants";
16
- import { mfaManageErrorKey } from "./mfa-error-keys";
17
16
 
18
17
  export type MfaDisableDialogProps = {
19
18
  readonly open: boolean;
@@ -37,7 +36,7 @@ export function MfaDisableDialog({
37
36
  const res = await dispatcher.write<{ disabled: boolean }>(AuthMfaHandlers.disable, { code });
38
37
  setCode("");
39
38
  if (!res.isSuccess) {
40
- onError(mfaManageErrorKey(res.error.code));
39
+ onError(res.error.i18nKey);
41
40
  return;
42
41
  }
43
42
  onDisabled();
@@ -21,7 +21,6 @@ import { type ReactNode, useState } from "react";
21
21
  // kumiko-lint-ignore cross-feature-import client-only hook, the feature's server barrel has no web/ re-export
22
22
  import { useSession } from "../../auth-email-password/web";
23
23
  import { AuthMfaHandlers } from "../constants";
24
- import { mfaManageErrorKey } from "./mfa-error-keys";
25
24
 
26
25
  type EnableStartResponse = {
27
26
  readonly setupToken: string;
@@ -77,7 +76,7 @@ export function MfaEnableScreen({
77
76
  accountLabel: session.user?.email ?? "",
78
77
  });
79
78
  if (!res.isSuccess) {
80
- setError(res.error.code);
79
+ setError(res.error.i18nKey);
81
80
  return;
82
81
  }
83
82
  // errorCorrectionLevel "H" (~30% redundancy) — more resilient to
@@ -96,7 +95,7 @@ export function MfaEnableScreen({
96
95
  setAcknowledged(false);
97
96
  setCode("");
98
97
  } catch {
99
- setError("setup_failed");
98
+ setError("auth.mfa.errors.setupFailed");
100
99
  } finally {
101
100
  setBusy(false);
102
101
  }
@@ -112,14 +111,14 @@ export function MfaEnableScreen({
112
111
  code,
113
112
  });
114
113
  if (!res.isSuccess) {
115
- setError(res.error.code);
114
+ setError(res.error.i18nKey);
116
115
  return;
117
116
  }
118
117
  setEnabled(true);
119
118
  setSetup(null);
120
119
  onEnabled?.();
121
120
  } catch {
122
- setError("setup_failed");
121
+ setError("auth.mfa.errors.setupFailed");
123
122
  } finally {
124
123
  setBusy(false);
125
124
  }
@@ -130,7 +129,7 @@ export function MfaEnableScreen({
130
129
  <Heading>{t("auth.mfa.enable.title")}</Heading>
131
130
 
132
131
  {enabled && <Banner variant="info">{t("auth.mfa.enable.success")}</Banner>}
133
- {error !== null && <Banner variant="error">{t(mfaManageErrorKey(error))}</Banner>}
132
+ {error !== null && <Banner variant="error">{t(error)}</Banner>}
134
133
 
135
134
  {!setup && !enabled && (
136
135
  <Section
@@ -10,7 +10,6 @@
10
10
  import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
11
11
  import { type ReactNode, useState } from "react";
12
12
  import { AuthMfaHandlers } from "../constants";
13
- import { mfaManageErrorKey } from "./mfa-error-keys";
14
13
 
15
14
  export type MfaRegenerateRecoveryDialogProps = {
16
15
  readonly open: boolean;
@@ -37,7 +36,7 @@ export function MfaRegenerateRecoveryDialog({
37
36
  );
38
37
  setCode("");
39
38
  if (!res.isSuccess) {
40
- onError(mfaManageErrorKey(res.error.code));
39
+ onError(res.error.i18nKey);
41
40
  return;
42
41
  }
43
42
  onRegenerated(res.data.recoveryCodes);
@@ -25,9 +25,14 @@ export const invitationsQuery = defineQueryHandler({
25
25
  return Promise.all(
26
26
  (rows ?? []).map(async (row) => {
27
27
  const email = row["email"];
28
- return typeof email === "string"
29
- ? { ...row, email: await decryptStoredPii(email, "tenant:invitations") }
30
- : row;
28
+ const invitedBy = row["invitedBy"];
29
+ const decryptedEmail =
30
+ typeof email === "string" ? await decryptStoredPii(email, "tenant:invitations") : email;
31
+ const decryptedInvitedBy =
32
+ typeof invitedBy === "string"
33
+ ? await decryptStoredPii(invitedBy, "tenant:invitations")
34
+ : invitedBy;
35
+ return { ...row, email: decryptedEmail, invitedBy: decryptedInvitedBy };
31
36
  }),
32
37
  );
33
38
  },
@@ -2,6 +2,7 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
4
4
  import { z } from "zod";
5
+ import { decryptStoredPii } from "../../shared";
5
6
  import { userTable } from "../../user";
6
7
  import { tenantMembershipsTable } from "../membership-table";
7
8
 
@@ -21,16 +22,24 @@ export const membersQuery = defineQueryHandler({
21
22
  userIds.length > 0 ? await selectMany<UserRow>(ctx.db, userTable, { id: userIds }) : [];
22
23
  const userById = new Map(users.map((u) => [String(u.id), u]));
23
24
 
24
- return rows.map((row) => {
25
- const user = userById.get(String(row["userId"]));
26
- const email = typeof user?.email === "string" ? user.email : null;
27
- const displayName = typeof user?.displayName === "string" ? user.displayName : null;
28
- return {
29
- ...row,
30
- email,
31
- displayName,
32
- roles: parseRoles(row["roles"]),
33
- };
34
- });
25
+ return Promise.all(
26
+ rows.map(async (row) => {
27
+ const user = userById.get(String(row["userId"]));
28
+ const email =
29
+ typeof user?.email === "string"
30
+ ? await decryptStoredPii(user.email, "tenant:members")
31
+ : null;
32
+ const displayName =
33
+ typeof user?.displayName === "string"
34
+ ? await decryptStoredPii(user.displayName, "tenant:members")
35
+ : null;
36
+ return {
37
+ ...row,
38
+ email,
39
+ displayName,
40
+ roles: parseRoles(row["roles"]),
41
+ };
42
+ }),
43
+ );
35
44
  },
36
45
  });
@@ -1,20 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { AuthMfaErrorCodes } from "../../constants";
3
- import { defaultTranslations } from "../i18n";
4
- import { mfaManageErrorKey } from "../mfa-error-keys";
5
-
6
- describe("mfaManageErrorKey", () => {
7
- for (const code of Object.values(AuthMfaErrorCodes)) {
8
- test(`${code} maps to a key registered in en/de translations`, () => {
9
- const key = mfaManageErrorKey(code);
10
- expect(defaultTranslations["en"]?.[key]).toBeDefined();
11
- expect(defaultTranslations["de"]?.[key]).toBeDefined();
12
- });
13
- }
14
-
15
- test("unknown code falls back to the generic verifyFailed key", () => {
16
- const key = mfaManageErrorKey("some_future_server_code");
17
- expect(key).toBe("auth.mfa.errors.verifyFailed");
18
- expect(defaultTranslations["en"]?.[key]).toBeDefined();
19
- });
20
- });
@@ -1,29 +0,0 @@
1
- // @runtime client
2
- // Shared error-code → i18n-key mapping for the account-management write
3
- // handlers (enable-confirm/disable/regenerate-recovery) — distinct from
4
- // mfa-verify-screen.tsx's own mapper, which covers the login-challenge
5
- // error surface (challenge_expired/rate_limited) that these handlers never
6
- // return. Centralized so enable/disable/regenerate stay in sync instead of
7
- // each re-deriving the key ad hoc (enable-screen used to template-string
8
- // the raw snake_case code straight into the key, which never matched the
9
- // camelCase keys actually registered below).
10
- import { AuthMfaErrorCodes } from "../constants";
11
-
12
- export function mfaManageErrorKey(code: string): string {
13
- switch (code) {
14
- case AuthMfaErrorCodes.invalidTotpCode:
15
- return "auth.mfa.errors.invalidCode";
16
- case AuthMfaErrorCodes.invalidRecoveryCode:
17
- return "auth.mfa.errors.invalidRecoveryCode";
18
- case AuthMfaErrorCodes.mfaAlreadyEnabled:
19
- return "auth.mfa.errors.mfaAlreadyEnabled";
20
- case AuthMfaErrorCodes.mfaNotEnabled:
21
- return "auth.mfa.errors.mfaNotEnabled";
22
- case AuthMfaErrorCodes.invalidSetupToken:
23
- return "auth.mfa.errors.invalidSetupToken";
24
- case AuthMfaErrorCodes.setupFailed:
25
- return "auth.mfa.errors.setupFailed";
26
- default:
27
- return "auth.mfa.errors.verifyFailed";
28
- }
29
- }