@cosmicdrift/kumiko-bundled-features 0.149.2 → 0.151.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 +7 -6
- package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +1 -1
- package/src/audit/__tests__/audit-security.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/account-lockout.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/auth-claims.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/auth.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-verification.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/invite-flow.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/multi-roles.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/password-reset.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/seed-admin.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +1 -1
- package/src/auth-email-password/handlers/change-password.write.ts +1 -1
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +1 -2
- package/src/auth-email-password/handlers/login.write.ts +1 -1
- package/src/auth-email-password/handlers/reset-password.write.ts +1 -1
- package/src/auth-email-password/i18n.ts +4 -0
- package/src/auth-email-password/index.ts +6 -1
- package/src/auth-email-password/seeding.ts +1 -1
- package/src/auth-email-password/web/auth-gate.tsx +65 -20
- package/src/auth-email-password/web/index.ts +2 -4
- package/src/auth-email-password/web/login-screen.tsx +2 -0
- package/src/auth-mfa/__tests__/base32.test.ts +28 -0
- package/src/auth-mfa/__tests__/disable-and-regenerate.integration.test.ts +16 -0
- package/src/auth-mfa/__tests__/enable-flow.integration.test.ts +60 -0
- package/src/auth-mfa/__tests__/pii-subject-encryption.integration.test.ts +2 -0
- package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +2 -0
- package/src/auth-mfa/__tests__/session-auto-revoke.integration.test.ts +2 -1
- package/src/auth-mfa/__tests__/totp.test.ts +11 -2
- package/src/auth-mfa/__tests__/verify.integration.test.ts +67 -1
- package/src/auth-mfa/base32.ts +7 -1
- package/src/auth-mfa/constants.ts +14 -0
- package/src/auth-mfa/errors.ts +9 -16
- package/src/auth-mfa/feature.ts +5 -1
- package/src/auth-mfa/handlers/enable-confirm.write.ts +20 -1
- package/src/auth-mfa/handlers/verify.write.ts +26 -3
- package/src/auth-mfa/index.ts +0 -1
- package/src/auth-mfa/recovery-codes.ts +12 -2
- package/src/auth-mfa/schema/user-mfa.ts +3 -0
- package/src/auth-mfa/testing.ts +5 -0
- package/src/auth-mfa/web/__tests__/mfa-error-keys.test.ts +20 -0
- package/src/auth-mfa/web/i18n.ts +2 -0
- package/src/auth-mfa/web/mfa-enable-screen.tsx +44 -28
- package/src/auth-mfa/web/mfa-error-keys.ts +7 -5
- package/src/auth-mfa/web/mfa-verify-screen.tsx +11 -0
- package/src/auth-mfa/web/qrcode-browser.d.ts +7 -0
- package/src/auth-mfa-user-data/__tests__/hooks.integration.test.ts +2 -0
- package/src/config/__tests__/config.integration.test.ts +50 -2
- package/src/config/handlers/cascade.query.ts +2 -1
- package/src/config/handlers/values.query.ts +2 -1
- package/src/file-foundation/__tests__/feature.test.ts +4 -4
- package/src/file-foundation/feature.ts +10 -11
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +15 -2
- package/src/legal-pages/feature.ts +2 -1
- package/src/mail-foundation/__tests__/feature.test.ts +4 -5
- package/src/mail-foundation/feature.ts +21 -22
- package/src/managed-pages/feature.ts +1 -6
- package/src/seo/feature.ts +1 -6
- package/src/sessions/__tests__/sessions-screens.boot.test.ts +51 -0
- package/src/sessions/__tests__/sessions.integration.test.ts +44 -0
- package/src/sessions/__tests__/test-helpers.ts +1 -1
- package/src/sessions/constants.ts +5 -0
- package/src/sessions/feature.ts +67 -1
- package/src/sessions/handlers/detail.query.ts +36 -0
- package/src/sessions/i18n.ts +23 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/system-query.ts +5 -0
- package/src/tenant/__tests__/tenant-security.integration.test.ts +1 -1
- package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +163 -0
- package/src/user-data-rights/__tests__/download.integration.test.ts +39 -0
- package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +1 -1
- package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +1 -1
- package/src/user-data-rights/feature.ts +7 -8
- package/src/user-data-rights/handlers/download-by-job.query.ts +5 -2
- package/src/user-data-rights/handlers/download-by-token.query.ts +5 -4
- package/src/user-data-rights/lib/tenant-file-provider.ts +62 -0
- package/src/user-profile/__tests__/change-email.integration.test.ts +1 -1
- package/src/user-profile/handlers/change-email.write.ts +2 -2
- package/src/auth-email-password/password-hashing.ts +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.151.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>",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"./auth-mfa": "./src/auth-mfa/index.ts",
|
|
92
92
|
"./auth-mfa-user-data": "./src/auth-mfa-user-data/index.ts",
|
|
93
93
|
"./auth-mfa/web": "./src/auth-mfa/web/index.ts",
|
|
94
|
+
"./auth-mfa/testing": "./src/auth-mfa/testing.ts",
|
|
94
95
|
"./sessions/testing": "./src/sessions/testing.ts",
|
|
95
96
|
"./personal-access-tokens": "./src/personal-access-tokens/index.ts",
|
|
96
97
|
"./personal-access-tokens/web": "./src/personal-access-tokens/web/index.ts",
|
|
@@ -114,11 +115,11 @@
|
|
|
114
115
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
115
116
|
},
|
|
116
117
|
"dependencies": {
|
|
117
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
118
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
119
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
120
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
121
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
118
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.151.0",
|
|
119
|
+
"@cosmicdrift/kumiko-framework": "0.151.0",
|
|
120
|
+
"@cosmicdrift/kumiko-headless": "0.151.0",
|
|
121
|
+
"@cosmicdrift/kumiko-renderer": "0.151.0",
|
|
122
|
+
"@cosmicdrift/kumiko-renderer-web": "0.151.0",
|
|
122
123
|
"@mollie/api-client": "^4.5.0",
|
|
123
124
|
"imapflow": "^1.3.3",
|
|
124
125
|
"mailparser": "^3.9.8",
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
unsafePushTables,
|
|
17
17
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
18
18
|
import { createAuditFeature } from "../../audit/feature";
|
|
19
|
-
import { hashPassword } from "../../auth-email-password/password-hashing";
|
|
20
19
|
import { ConfigQueries } from "../../config/constants";
|
|
21
20
|
import { createConfigFeature } from "../../config/feature";
|
|
22
21
|
import { createConfigResolver } from "../../config/resolver";
|
|
@@ -26,6 +25,7 @@ import { globalFeatureStateTable } from "../../feature-toggles/global-feature-st
|
|
|
26
25
|
import { JobQueries } from "../../jobs/constants";
|
|
27
26
|
import { createJobsFeature } from "../../jobs/feature";
|
|
28
27
|
import { jobRunLogsTable, jobRunsTable } from "../../jobs/job-run-table";
|
|
28
|
+
import { hashPassword } from "../../shared";
|
|
29
29
|
import { TenantQueries } from "../../tenant/constants";
|
|
30
30
|
import { createTenantFeature } from "../../tenant/feature";
|
|
31
31
|
import { tenantInvitationEntity } from "../../tenant/invitation-table";
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
unsafePushTables,
|
|
24
24
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
25
25
|
import { rolesOf } from "@cosmicdrift/kumiko-framework/testing";
|
|
26
|
-
import { hashPassword } from "../../auth-email-password/password-hashing";
|
|
27
26
|
import { createConfigFeature } from "../../config/feature";
|
|
27
|
+
import { hashPassword } from "../../shared";
|
|
28
28
|
import { createTenantFeature } from "../../tenant/feature";
|
|
29
29
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
30
30
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -25,6 +25,7 @@ import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing"
|
|
|
25
25
|
import { createConfigFeature } from "../../config";
|
|
26
26
|
import { createConfigResolver } from "../../config/resolver";
|
|
27
27
|
import { configValuesTable } from "../../config/table";
|
|
28
|
+
import { hashPassword } from "../../shared";
|
|
28
29
|
import { createTenantFeature } from "../../tenant";
|
|
29
30
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
30
31
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -34,7 +35,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
34
35
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
35
36
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
36
37
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
37
|
-
import { hashPassword } from "../password-hashing";
|
|
38
38
|
|
|
39
39
|
let stack: TestStack;
|
|
40
40
|
|
|
@@ -24,6 +24,7 @@ import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing"
|
|
|
24
24
|
import { createConfigFeature } from "../../config";
|
|
25
25
|
import { createConfigResolver } from "../../config/resolver";
|
|
26
26
|
import { configValuesTable } from "../../config/table";
|
|
27
|
+
import { hashPassword } from "../../shared";
|
|
27
28
|
import { createTenantFeature } from "../../tenant";
|
|
28
29
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
29
30
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -34,7 +35,6 @@ import { userEntity, userTable } from "../../user/schema/user";
|
|
|
34
35
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
35
36
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
36
37
|
import { getLockoutState, type LockoutState } from "../lockout-store";
|
|
37
|
-
import { hashPassword } from "../password-hashing";
|
|
38
38
|
|
|
39
39
|
let stack: TestStack;
|
|
40
40
|
|
|
@@ -15,6 +15,7 @@ import { createTestEnvelopeCipher, seedRow } from "@cosmicdrift/kumiko-framework
|
|
|
15
15
|
import { createConfigFeature } from "../../config";
|
|
16
16
|
import { createConfigResolver } from "../../config/resolver";
|
|
17
17
|
import { configValuesTable } from "../../config/table";
|
|
18
|
+
import { hashPassword } from "../../shared";
|
|
18
19
|
import { createTenantFeature } from "../../tenant";
|
|
19
20
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
20
21
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -24,7 +25,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
24
25
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
25
26
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
26
27
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
27
|
-
import { hashPassword } from "../password-hashing";
|
|
28
28
|
|
|
29
29
|
// Sample-style extension feature that shows the real-world shape of a claims
|
|
30
30
|
// hook: look something up in a tenant-scoped table and stuff it into the JWT.
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { createConfigFeature } from "../../config";
|
|
21
21
|
import { createConfigResolver } from "../../config/resolver";
|
|
22
22
|
import { configValuesTable } from "../../config/table";
|
|
23
|
+
import { hashPassword } from "../../shared";
|
|
23
24
|
import { createTenantFeature } from "../../tenant";
|
|
24
25
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
25
26
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -29,7 +30,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
29
30
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
30
31
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
31
32
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
32
|
-
import { hashPassword } from "../password-hashing";
|
|
33
33
|
|
|
34
34
|
let stack: TestStack;
|
|
35
35
|
|
|
@@ -23,6 +23,7 @@ import { createDeliveryFeature, createDeliveryTestContext } from "../../delivery
|
|
|
23
23
|
import { notificationPreferencesTable } from "../../delivery/tables";
|
|
24
24
|
import { createRendererFoundationFeature } from "../../renderer-foundation/feature";
|
|
25
25
|
import { createRendererSimpleFeature, simpleRenderer } from "../../renderer-simple";
|
|
26
|
+
import { hashPassword } from "../../shared";
|
|
26
27
|
import { createTemplateResolverFeature } from "../../template-resolver/feature";
|
|
27
28
|
import { createTenantFeature } from "../../tenant";
|
|
28
29
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
@@ -33,7 +34,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
33
34
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
34
35
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
35
36
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
36
|
-
import { hashPassword } from "../password-hashing";
|
|
37
37
|
import { signResetToken } from "../reset-token";
|
|
38
38
|
import { signVerificationToken } from "../verification-token";
|
|
39
39
|
|
|
@@ -31,6 +31,7 @@ import { createDeliveryFeature, createDeliveryTestContext } from "../../delivery
|
|
|
31
31
|
import { notificationPreferencesTable } from "../../delivery/tables";
|
|
32
32
|
import { createRendererFoundationFeature } from "../../renderer-foundation/feature";
|
|
33
33
|
import { createRendererSimpleFeature, simpleRenderer } from "../../renderer-simple";
|
|
34
|
+
import { hashPassword } from "../../shared";
|
|
34
35
|
import { createTemplateResolverFeature } from "../../template-resolver/feature";
|
|
35
36
|
import { createTenantFeature } from "../../tenant";
|
|
36
37
|
import { tenantInvitationEntity, tenantInvitationsTable } from "../../tenant/invitation-table";
|
|
@@ -41,7 +42,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
41
42
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
42
43
|
import { AuthHandlers } from "../constants";
|
|
43
44
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
44
|
-
import { hashPassword } from "../password-hashing";
|
|
45
45
|
import { seedUser } from "../seeding";
|
|
46
46
|
|
|
47
47
|
const APP_ACCEPT_URL = "https://app.example.com/invite/accept";
|
|
@@ -37,6 +37,7 @@ import { createDeliveryFeature, createDeliveryTestContext } from "../../delivery
|
|
|
37
37
|
import { notificationPreferencesTable } from "../../delivery/tables";
|
|
38
38
|
import { createRendererFoundationFeature } from "../../renderer-foundation/feature";
|
|
39
39
|
import { createRendererSimpleFeature, simpleRenderer } from "../../renderer-simple";
|
|
40
|
+
import { hashPassword } from "../../shared";
|
|
40
41
|
import { createTemplateResolverFeature } from "../../template-resolver/feature";
|
|
41
42
|
import { createTenantFeature } from "../../tenant";
|
|
42
43
|
import {
|
|
@@ -52,7 +53,6 @@ import { userEntity, userTable } from "../../user/schema/user";
|
|
|
52
53
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
53
54
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
54
55
|
import { storeInviteToken } from "../invite-token-store";
|
|
55
|
-
import { hashPassword } from "../password-hashing";
|
|
56
56
|
import { seedUser } from "../seeding";
|
|
57
57
|
|
|
58
58
|
const APP_ACCEPT_URL = "https://app.example.com/invite/accept";
|
|
@@ -23,6 +23,7 @@ import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
|
|
|
23
23
|
import { createConfigFeature } from "../../config";
|
|
24
24
|
import { createConfigResolver } from "../../config/resolver";
|
|
25
25
|
import { configValuesTable } from "../../config/table";
|
|
26
|
+
import { hashPassword } from "../../shared";
|
|
26
27
|
import { createTenantFeature } from "../../tenant";
|
|
27
28
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
28
29
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -31,7 +32,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
31
32
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
32
33
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
33
34
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
34
|
-
import { hashPassword } from "../password-hashing";
|
|
35
35
|
|
|
36
36
|
let stack: TestStack;
|
|
37
37
|
const systemAdmin = TestUsers.systemAdmin;
|
|
@@ -20,6 +20,7 @@ import { notificationPreferencesTable } from "../../delivery/tables";
|
|
|
20
20
|
import { createRendererFoundationFeature } from "../../renderer-foundation/feature";
|
|
21
21
|
import { createRendererSimpleFeature, simpleRenderer } from "../../renderer-simple";
|
|
22
22
|
import { createSessionsFeature, userSessionTable } from "../../sessions";
|
|
23
|
+
import { hashPassword, verifyPassword } from "../../shared";
|
|
23
24
|
import { createTemplateResolverFeature } from "../../template-resolver/feature";
|
|
24
25
|
import { createTenantFeature } from "../../tenant";
|
|
25
26
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
@@ -30,7 +31,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
30
31
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
31
32
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
32
33
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
33
|
-
import { hashPassword, verifyPassword } from "../password-hashing";
|
|
34
34
|
import { signResetToken } from "../reset-token";
|
|
35
35
|
|
|
36
36
|
// Reset mails now go through delivery (ctx.notify → channel-email). The
|
|
@@ -23,12 +23,12 @@ import {
|
|
|
23
23
|
import { createConfigFeature } from "../../config/feature";
|
|
24
24
|
import { createConfigResolver } from "../../config/resolver";
|
|
25
25
|
import { configValuesTable } from "../../config/table";
|
|
26
|
+
import { verifyPassword } from "../../shared";
|
|
26
27
|
import { createTenantFeature } from "../../tenant/feature";
|
|
27
28
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
28
29
|
import { tenantEntity, tenantTable } from "../../tenant/schema/tenant";
|
|
29
30
|
import { createUserFeature } from "../../user/feature";
|
|
30
31
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
31
|
-
import { verifyPassword } from "../password-hashing";
|
|
32
32
|
import { seedAdmin } from "../testing";
|
|
33
33
|
|
|
34
34
|
let stack: TestStack;
|
|
@@ -14,6 +14,7 @@ import * as jose from "jose";
|
|
|
14
14
|
import { createConfigFeature } from "../../config";
|
|
15
15
|
import { createConfigResolver } from "../../config/resolver";
|
|
16
16
|
import { configValuesTable } from "../../config/table";
|
|
17
|
+
import { hashPassword } from "../../shared";
|
|
17
18
|
import { createTenantFeature } from "../../tenant";
|
|
18
19
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
19
20
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -23,7 +24,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
23
24
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
24
25
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
25
26
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
26
|
-
import { hashPassword } from "../password-hashing";
|
|
27
27
|
|
|
28
28
|
// In-memory fake of a real sessions-store — just enough to observe that the
|
|
29
29
|
// framework calls the callbacks at the right moments and threads the sid back
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
SYSTEM_TENANT_ID,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { hashPassword, verifyPassword } from "../../shared";
|
|
8
9
|
import { UserHandlers, UserQueries } from "../../user";
|
|
9
10
|
import { invalidCredentials } from "../errors";
|
|
10
|
-
import { hashPassword, verifyPassword } from "../password-hashing";
|
|
11
11
|
|
|
12
12
|
// Change-password — authenticated. The user supplies their current password
|
|
13
13
|
// (re-auth) and the new one. The new hash is written via ctx.writeAs(system)
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
29
29
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
30
30
|
import { z } from "zod";
|
|
31
|
-
import { decryptStoredPii } from "../../shared";
|
|
31
|
+
import { decryptStoredPii, verifyPassword } from "../../shared";
|
|
32
32
|
// kumiko-lint-ignore cross-feature-import invite-flow
|
|
33
33
|
import {
|
|
34
34
|
INVITATION_STATUS,
|
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
getInvitationIdForToken,
|
|
47
47
|
unburnInviteToken,
|
|
48
48
|
} from "../invite-token-store";
|
|
49
|
-
import { verifyPassword } from "../password-hashing";
|
|
50
49
|
|
|
51
50
|
const InviteAcceptWithLoginSchema = z.object({
|
|
52
51
|
token: z.string().min(1),
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
10
10
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
11
11
|
import { z } from "zod";
|
|
12
|
+
import { verifyDummyPassword, verifyPassword } from "../../shared";
|
|
12
13
|
import { USER_STATUS, UserQueries } from "../../user";
|
|
13
14
|
import { parseAuthUserRow } from "../auth-user-row";
|
|
14
15
|
import {
|
|
@@ -23,7 +24,6 @@ import {
|
|
|
23
24
|
noMembership,
|
|
24
25
|
} from "../errors";
|
|
25
26
|
import { clearLockoutState, getLockoutState, recordFailedAttempt } from "../lockout-store";
|
|
26
|
-
import { verifyDummyPassword, verifyPassword } from "../password-hashing";
|
|
27
27
|
|
|
28
28
|
export type LoginHandlerOptions = {
|
|
29
29
|
// When true, a valid (email + password) login fails with email_not_verified
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { hashPassword } from "../../shared";
|
|
4
5
|
import { AuthErrors } from "../constants";
|
|
5
6
|
import { invalidResetToken } from "../errors";
|
|
6
|
-
import { hashPassword } from "../password-hashing";
|
|
7
7
|
import { verifyResetToken } from "../reset-token";
|
|
8
8
|
import { runConfirmTokenFlow } from "./confirm-token-flow";
|
|
9
9
|
|
|
@@ -32,6 +32,8 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
32
32
|
"auth.errors.rateLimited": "Zu viele Login-Versuche. Bitte kurz warten.",
|
|
33
33
|
"auth.errors.invalidBody": "Ungültige Eingabe.",
|
|
34
34
|
"auth.errors.loginFailed": "Login fehlgeschlagen.",
|
|
35
|
+
"auth.errors.mfaNotSupported":
|
|
36
|
+
"Diese App unterstützt keine Zwei-Faktor-Bestätigung. Bitte an den Support wenden.",
|
|
35
37
|
"auth.errors.invalidResetToken":
|
|
36
38
|
"Der Link ist ungültig oder abgelaufen. Bitte fordere einen neuen an.",
|
|
37
39
|
"auth.errors.invalidVerificationToken": "Der Bestätigungs-Link ist ungültig oder abgelaufen.",
|
|
@@ -134,6 +136,8 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
134
136
|
"auth.errors.rateLimited": "Too many login attempts. Please wait briefly.",
|
|
135
137
|
"auth.errors.invalidBody": "Invalid input.",
|
|
136
138
|
"auth.errors.loginFailed": "Login failed.",
|
|
139
|
+
"auth.errors.mfaNotSupported":
|
|
140
|
+
"This app doesn't support two-factor verification. Please contact support.",
|
|
137
141
|
"auth.errors.invalidResetToken": "Link is invalid or expired. Please request a new one.",
|
|
138
142
|
"auth.errors.invalidVerificationToken": "Verification link is invalid or expired.",
|
|
139
143
|
"auth.errors.invalidSignupToken":
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// Convention paths for the auth pages (reset/verify/signup/invite); apps build
|
|
2
2
|
// their magic-link appUrls from these. All mail now goes through delivery.
|
|
3
|
+
|
|
4
|
+
// Kept as a public re-export (not just internal call sites) — this feature's
|
|
5
|
+
// barrel is where consumers of the published package have always found
|
|
6
|
+
// hashPassword/verifyPassword; removing it is a breaking change that needs
|
|
7
|
+
// its own changeset + deliberate deprecation window, not a silent drop.
|
|
8
|
+
export { hashPassword, verifyPassword } from "../shared/password-hashing";
|
|
3
9
|
export { type AuthPaths, DEFAULT_AUTH_PATHS, makeAuthPaths } from "./auth-paths";
|
|
4
10
|
export { AUTH_EMAIL_PASSWORD_FEATURE, AuthErrors, AuthHandlers } from "./constants";
|
|
5
11
|
// Renderers for the auth mails. All four magic-link flows (reset, verify,
|
|
@@ -27,7 +33,6 @@ export type {
|
|
|
27
33
|
SignupOptions,
|
|
28
34
|
} from "./feature";
|
|
29
35
|
export { authEmailPasswordEnvSchema, createAuthEmailPasswordFeature } from "./feature";
|
|
30
|
-
export { hashPassword, verifyPassword } from "./password-hashing";
|
|
31
36
|
// Generic HMAC-signed single-purpose token helpers. Re-exported damit
|
|
32
37
|
// app-spezifische out-of-band-Flows (subscriber-confirm, magic-links,
|
|
33
38
|
// invite-tokens) denselben battle-tested signer/verifier nutzen können
|
|
@@ -17,13 +17,13 @@ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
|
17
17
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
18
18
|
import { ConflictError } from "@cosmicdrift/kumiko-framework/errors";
|
|
19
19
|
import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
20
|
+
import { hashPassword } from "../shared";
|
|
20
21
|
// kumiko-lint-ignore cross-feature-import auth-tests need user+tenant seed-helpers
|
|
21
22
|
import { seedTenant, seedTenantMembership } from "../tenant/seeding";
|
|
22
23
|
// kumiko-lint-ignore cross-feature-import signup create-only guard reads the user projection by email
|
|
23
24
|
import { userTable } from "../user/schema/user";
|
|
24
25
|
// kumiko-lint-ignore cross-feature-import auth-tests need user+tenant seed-helpers
|
|
25
26
|
import { seedUser } from "../user/seeding";
|
|
26
|
-
import { hashPassword } from "./password-hashing";
|
|
27
27
|
|
|
28
28
|
// Re-export für ergonomische Single-Import-Site in tests/seed-scripts.
|
|
29
29
|
// Das Auth-Feature ist der natürliche Aufrufer für "seed admin user mit
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// (nur `{ children }`-Prop). Der Sample kann so einen eigenen Login-
|
|
10
10
|
// Screen rein konfigurieren, ohne den Gate selbst ersetzen zu müssen.
|
|
11
11
|
|
|
12
|
-
import { type ComponentType, type ReactNode, useState } from "react";
|
|
12
|
+
import { type ComponentType, type ReactNode, useEffect, useState } from "react";
|
|
13
13
|
import { LoginScreen, type LoginScreenProps } from "./login-screen";
|
|
14
14
|
import { SessionProvider, useSession } from "./session";
|
|
15
15
|
|
|
@@ -20,42 +20,87 @@ import { SessionProvider, useSession } from "./session";
|
|
|
20
20
|
export type MfaVerifyComponentProps = {
|
|
21
21
|
readonly challengeToken: string;
|
|
22
22
|
readonly onSuccess?: () => void;
|
|
23
|
+
readonly onCancel?: () => void;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
export type LoginRouteOptions = {
|
|
27
|
+
readonly loginScreen?: ComponentType<LoginScreenProps>;
|
|
28
|
+
readonly loginScreenProps?: LoginScreenProps;
|
|
29
|
+
readonly mfaVerifyScreen?: ComponentType<MfaVerifyComponentProps>;
|
|
30
|
+
/** Called once the session becomes authenticated. makeAuthGate ignores
|
|
31
|
+
* this (it renders `children` on its own authenticated branch instead);
|
|
32
|
+
* standalone routes — no parent gate, e.g. an anonymous apex/marketing
|
|
33
|
+
* surface — use it to redirect. */
|
|
34
|
+
readonly onAuthenticated?: () => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// The one sanctioned way to render a login flow that correctly completes
|
|
38
|
+
// an MFA challenge — makeAuthGate below is just this wrapped for the
|
|
39
|
+
// `{ children }` gate shape. Also exported directly for apps that render
|
|
40
|
+
// their own standalone login route outside any gate (createPublicSurface
|
|
41
|
+
// apex pages stack providers, not gates — see auth-mount.tsx recipes).
|
|
42
|
+
// Handing an app raw LoginScreen + telling it to hand-roll the challenge-
|
|
43
|
+
// token swap itself is exactly how kumiko-framework#266's login-time MFA
|
|
44
|
+
// step went missing in a real apex surface; this makes that mistake
|
|
45
|
+
// structurally unavailable — there is no lower-level piece left to misuse.
|
|
46
|
+
export function createLoginRoute(
|
|
47
|
+
opts: LoginRouteOptions = {},
|
|
48
|
+
): ComponentType<Record<string, never>> {
|
|
49
|
+
const LoginComponent = opts.loginScreen ?? LoginScreen;
|
|
50
|
+
const MfaVerifyComponent = opts.mfaVerifyScreen;
|
|
51
|
+
|
|
52
|
+
function LoginRoute(): ReactNode {
|
|
31
53
|
const { status } = useSession();
|
|
32
54
|
// Pending challenge-token from LoginScreen's onMfaChallenge. Lives here
|
|
33
55
|
// (not in SessionState) because it's a UI-only transition — the server
|
|
34
56
|
// never considers this session authenticated until verify succeeds.
|
|
35
57
|
const [challengeToken, setChallengeToken] = useState<string | null>(null);
|
|
36
58
|
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (status === "authenticated") opts.onAuthenticated?.();
|
|
61
|
+
}, [status]);
|
|
62
|
+
|
|
37
63
|
if (status === "loading") {
|
|
38
64
|
return (
|
|
39
65
|
<div className="min-h-screen flex items-center justify-center text-muted-foreground text-sm" />
|
|
40
66
|
);
|
|
41
67
|
}
|
|
42
|
-
if (status === "
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<MfaVerifyComponent
|
|
46
|
-
challengeToken={challengeToken}
|
|
47
|
-
onSuccess={() => setChallengeToken(null)}
|
|
48
|
-
/>
|
|
49
|
-
);
|
|
50
|
-
}
|
|
68
|
+
if (status === "authenticated") return null;
|
|
69
|
+
if (challengeToken !== null && MfaVerifyComponent) {
|
|
51
70
|
return (
|
|
52
|
-
<
|
|
53
|
-
{
|
|
54
|
-
|
|
71
|
+
<MfaVerifyComponent
|
|
72
|
+
challengeToken={challengeToken}
|
|
73
|
+
onSuccess={() => setChallengeToken(null)}
|
|
74
|
+
onCancel={() => setChallengeToken(null)}
|
|
55
75
|
/>
|
|
56
76
|
);
|
|
57
77
|
}
|
|
58
|
-
return
|
|
78
|
+
return (
|
|
79
|
+
<LoginComponent
|
|
80
|
+
{...opts.loginScreenProps}
|
|
81
|
+
onMfaChallenge={
|
|
82
|
+
MfaVerifyComponent ? setChallengeToken : opts.loginScreenProps?.onMfaChallenge
|
|
83
|
+
}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return LoginRoute;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function makeAuthGate(
|
|
91
|
+
LoginComponent: ComponentType<LoginScreenProps> = LoginScreen,
|
|
92
|
+
loginProps?: LoginScreenProps,
|
|
93
|
+
MfaVerifyComponent?: ComponentType<MfaVerifyComponentProps>,
|
|
94
|
+
): ComponentType<{ children: ReactNode }> {
|
|
95
|
+
const LoginRoute = createLoginRoute({
|
|
96
|
+
loginScreen: LoginComponent,
|
|
97
|
+
loginScreenProps: loginProps,
|
|
98
|
+
mfaVerifyScreen: MfaVerifyComponent,
|
|
99
|
+
});
|
|
100
|
+
function AuthGate({ children }: { readonly children: ReactNode }): ReactNode {
|
|
101
|
+
const { status } = useSession();
|
|
102
|
+
if (status === "authenticated") return <>{children}</>;
|
|
103
|
+
return <LoginRoute />;
|
|
59
104
|
}
|
|
60
105
|
return AuthGate;
|
|
61
106
|
}
|
|
@@ -27,8 +27,8 @@ export {
|
|
|
27
27
|
} from "./auth-client";
|
|
28
28
|
export type { AuthCardProps, AuthShellRenderer } from "./auth-form-primitives";
|
|
29
29
|
export { AuthCard, AuthShellProvider, useAuthShell } from "./auth-form-primitives";
|
|
30
|
-
export type { MfaVerifyComponentProps } from "./auth-gate";
|
|
31
|
-
export { makeAuthGate, makeSessionAuthGate } from "./auth-gate";
|
|
30
|
+
export type { LoginRouteOptions, MfaVerifyComponentProps } from "./auth-gate";
|
|
31
|
+
export { createLoginRoute, makeAuthGate, makeSessionAuthGate } from "./auth-gate";
|
|
32
32
|
export type {
|
|
33
33
|
EmailPasswordClientFeature,
|
|
34
34
|
EmailPasswordClientOptions,
|
|
@@ -40,8 +40,6 @@ export type { ForgotPasswordScreenProps } from "./forgot-password-screen";
|
|
|
40
40
|
export { ForgotPasswordScreen } from "./forgot-password-screen";
|
|
41
41
|
export type { InviteAcceptScreenProps } from "./invite-accept-screen";
|
|
42
42
|
export { InviteAcceptScreen } from "./invite-accept-screen";
|
|
43
|
-
export type { LoginScreenProps } from "./login-screen";
|
|
44
|
-
export { LoginScreen } from "./login-screen";
|
|
45
43
|
export type { ResetPasswordScreenProps } from "./reset-password-screen";
|
|
46
44
|
export { ResetPasswordScreen } from "./reset-password-screen";
|
|
47
45
|
export type { SessionApi, SessionState, SessionStatus } from "./session";
|
|
@@ -88,6 +88,8 @@ function reasonToKey(failure: LoginFailure): {
|
|
|
88
88
|
return { key: "auth.errors.rateLimited" };
|
|
89
89
|
case "invalid_body":
|
|
90
90
|
return { key: "auth.errors.invalidBody" };
|
|
91
|
+
case "mfa_not_supported":
|
|
92
|
+
return { key: "auth.errors.mfaNotSupported" };
|
|
91
93
|
default:
|
|
92
94
|
return { key: "auth.errors.loginFailed" };
|
|
93
95
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { base32Decode, base32Encode } from "../base32";
|
|
3
|
+
|
|
4
|
+
describe("base32 encode/decode", () => {
|
|
5
|
+
test("round-trips arbitrary bytes", () => {
|
|
6
|
+
const bytes = Buffer.from("12345678901234567890", "ascii");
|
|
7
|
+
expect(base32Decode(base32Encode(bytes))).toEqual(bytes);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("decodes lowercase and RFC 4648 '=' padding", () => {
|
|
11
|
+
const bytes = Buffer.from("12345678901234567890", "ascii");
|
|
12
|
+
const encoded = base32Encode(bytes);
|
|
13
|
+
const paddingLength = (8 - (encoded.length % 8)) % 8;
|
|
14
|
+
const withPadding = encoded + "=".repeat(paddingLength);
|
|
15
|
+
expect(base32Decode(withPadding.toLowerCase())).toEqual(bytes);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("rejects a non-canonical length with non-zero residual bits", () => {
|
|
19
|
+
// "AA" decodes to 10 residual bits of all-zero value — canonical (padding
|
|
20
|
+
// zeros). A single stray char (5 bits, below the byte boundary but not a
|
|
21
|
+
// clean multiple) with non-zero value must be rejected, not truncated.
|
|
22
|
+
expect(() => base32Decode("B")).toThrow(/invalid length/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("throws on a genuinely invalid character", () => {
|
|
26
|
+
expect(() => base32Decode("01")).toThrow(/invalid character/);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { createConfigFeature } from "../../config";
|
|
15
15
|
import { createConfigResolver } from "../../config/resolver";
|
|
16
16
|
import { configValuesTable } from "../../config/table";
|
|
17
|
+
import { createTenantFeature } from "../../tenant";
|
|
17
18
|
import { createUserFeature } from "../../user/feature";
|
|
18
19
|
import { userEntity } from "../../user/schema/user";
|
|
19
20
|
import { base32Decode } from "../base32";
|
|
@@ -34,6 +35,7 @@ beforeAll(async () => {
|
|
|
34
35
|
features: [
|
|
35
36
|
createConfigFeature(),
|
|
36
37
|
createUserFeature(),
|
|
38
|
+
createTenantFeature(),
|
|
37
39
|
createAuthMfaFeature({
|
|
38
40
|
setupTokenSecret: SETUP_TOKEN_SECRET,
|
|
39
41
|
issuer: "Kumiko Test",
|
|
@@ -139,6 +141,20 @@ describe("disable", () => {
|
|
|
139
141
|
expect(res.disabled).toBe(true);
|
|
140
142
|
});
|
|
141
143
|
|
|
144
|
+
test("a recovery code typed lowercase and without the dash still matches", async () => {
|
|
145
|
+
const { user, recoveryCodes } = await enableMfaFor("disable-recovery-normalize-1");
|
|
146
|
+
const code = recoveryCodes[0];
|
|
147
|
+
if (code === undefined) throw new Error("test setup produced no recovery code");
|
|
148
|
+
|
|
149
|
+
const sloppy = code.toLowerCase().replace("-", "");
|
|
150
|
+
const res = await stack.http.writeOk<{ disabled: boolean }>(
|
|
151
|
+
AuthMfaHandlers.disable,
|
|
152
|
+
{ code: sloppy },
|
|
153
|
+
user,
|
|
154
|
+
);
|
|
155
|
+
expect(res.disabled).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
142
158
|
test("a wrong code is rejected and MFA stays enabled", async () => {
|
|
143
159
|
const { user, secret } = await enableMfaFor("disable-wrong-1");
|
|
144
160
|
|