@cosmicdrift/kumiko-bundled-features 0.165.0 → 2.0.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 +10 -6
- package/src/__tests__/access-invalidation-consumer.integration.test.ts +279 -0
- package/src/__tests__/access-invalidation-stream.integration.test.ts +217 -0
- package/src/audit/__tests__/audit.integration.test.ts +50 -21
- package/src/audit/handlers/list.query.ts +1 -0
- package/src/auth-email-password/__tests__/auth.integration.test.ts +6 -0
- package/src/auth-email-password/__tests__/invite-flow.integration.test.ts +34 -0
- package/src/auth-email-password/__tests__/self-registration-toggle.integration.test.ts +9 -14
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +16 -5
- package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +54 -0
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -1
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +2 -1
- package/src/auth-email-password/handlers/login.write.ts +8 -0
- package/src/auth-email-password/handlers/self-registration-status.query.ts +6 -3
- package/src/auth-email-password/handlers/signup-request.write.ts +12 -1
- package/src/auth-email-password/i18n.ts +2 -0
- package/src/auth-email-password/seeding.ts +41 -23
- package/src/auth-email-password/web/__tests__/auth-client.test.ts +13 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +55 -8
- package/src/auth-email-password/web/__tests__/client-plugin.test.tsx +34 -0
- package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +8 -1
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +5 -1
- package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +3 -0
- package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +8 -2
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +5 -1
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +5 -1
- package/src/auth-email-password/web/auth-client.ts +32 -10
- package/src/auth-email-password/web/auth-gate.tsx +17 -21
- package/src/auth-email-password/web/client-plugin.ts +15 -2
- package/src/auth-email-password/web/index.ts +5 -1
- package/src/auth-email-password/web/login-screen.tsx +7 -3
- package/src/auth-email-password/web/request-account-unlock-screen.tsx +1 -1
- package/src/auth-foundation/__tests__/session-store.test.ts +2 -4
- package/src/auth-foundation/__tests__/tenant-providers.test.ts +24 -0
- package/src/auth-foundation/__tests__/token-verifier.test.ts +36 -3
- package/src/auth-foundation/boot-checks.ts +74 -69
- package/src/auth-foundation/resolve-anonymous-access.ts +13 -5
- package/src/auth-foundation/types.ts +3 -3
- package/src/auth-mfa/__tests__/enable-confirm-preauth.integration.test.ts +61 -1
- package/src/auth-mfa/__tests__/no-redis.integration.test.ts +209 -0
- package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +79 -1
- package/src/auth-mfa/handlers/disable.write.ts +16 -1
- package/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +72 -42
- package/src/auth-mfa/handlers/enable-confirm.write.ts +23 -12
- package/src/auth-mfa/handlers/reencrypt.job.ts +26 -17
- package/src/auth-mfa/handlers/regenerate-recovery.write.ts +15 -1
- package/src/auth-mfa/handlers/verify.write.ts +28 -24
- package/src/auth-mfa/web/__tests__/mfa-client.test.ts +5 -1
- package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +10 -1
- package/src/auth-mfa/web/__tests__/mfa-setup-preauth-screen.test.tsx +104 -1
- package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +5 -1
- package/src/auth-mfa/web/i18n.ts +4 -0
- package/src/auth-mfa/web/mfa-client.ts +13 -2
- package/src/auth-mfa/web/mfa-enable-screen.tsx +1 -1
- package/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +25 -2
- package/src/billing-foundation/__tests__/subscription-provider-contract.ts +36 -12
- package/src/config/__tests__/backing-secrets.integration.test.ts +4 -0
- package/src/config/__tests__/cascade.integration.test.ts +13 -2
- package/src/config/__tests__/pii-encrypted.integration.test.ts +6 -6
- package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +78 -5
- package/src/config/db/queries/resolver.ts +3 -3
- package/src/config/handlers/reencrypt.job.ts +36 -24
- package/src/custom-fields/__tests__/custom-fields.integration.test.ts +14 -0
- package/src/document-ingest-foundation/__tests__/feature.test.ts +38 -0
- package/src/document-ingest-foundation/__tests__/pages.test.ts +37 -0
- package/src/document-ingest-foundation/entity.ts +10 -3
- package/src/document-ingest-foundation/feature.ts +26 -8
- package/src/document-ingest-foundation/index.ts +1 -0
- package/src/document-ingest-foundation/pages.ts +34 -0
- package/src/feature-toggles/__tests__/compose-tier-resolver.test.ts +114 -11
- package/src/feature-toggles/compose-tier-resolver.ts +77 -8
- package/src/feature-toggles/global-feature-state-table.ts +15 -12
- package/src/files-provider-s3/s3-provider.ts +12 -1
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +78 -1
- package/src/inbound-mail-foundation/__tests__/inbound-mail-provider-contract.ts +58 -40
- package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +9 -1
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +22 -12
- package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +1 -1
- package/src/inbound-provider-imap/__tests__/plugin-mocked.test.ts +35 -8
- package/src/inbound-provider-inmemory/__tests__/feature.test.ts +37 -33
- package/src/jobs/__tests__/job-instant.test.ts +15 -0
- package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +19 -9
- package/src/jobs/feature.ts +6 -5
- package/src/jobs/job-instant.ts +9 -0
- package/src/mail-foundation/__tests__/mail-transport-contract.ts +27 -11
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +10 -6
- package/src/mail-transport-smtp/__tests__/feature.test.ts +10 -5
- package/src/page-render/__tests__/markdown.test.ts +29 -0
- package/src/page-render/markdown.ts +1 -9
- package/src/personal-access-tokens/feature.ts +1 -1
- package/src/sessions/__tests__/rebuild-survival.integration.test.ts +3 -1
- package/src/sessions/__tests__/sessions-screens.boot.test.ts +5 -10
- package/src/sessions/__tests__/sessions.integration.test.ts +72 -1
- package/src/sessions/feature.ts +12 -0
- package/src/sessions/handlers/revoke-all-for-user.write.ts +51 -12
- package/src/sessions/handlers/revoke-all-others.write.ts +19 -1
- package/src/sessions/handlers/revoke.write.ts +13 -0
- package/src/sessions/index.ts +7 -0
- package/src/sessions/session-callbacks.ts +58 -5
- package/src/sessions/session-revoked-event.ts +29 -0
- package/src/shared/__tests__/chunked-entity-migration.test.ts +4 -4
- package/src/shared/chunked-entity-migration.ts +2 -2
- package/src/shared/grace-period.test.ts +26 -0
- package/src/shared/grace-period.ts +20 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/map-with-concurrency.test.ts +104 -0
- package/src/shared/map-with-concurrency.ts +38 -0
- package/src/subscription-mollie/__tests__/feature.test.ts +16 -6
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +21 -0
- package/src/subscription-mollie/verify-webhook.ts +1 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +14 -7
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +20 -0
- package/src/subscription-stripe/verify-webhook.ts +3 -2
- package/src/tenant/__tests__/members-query-kms.integration.test.ts +2 -2
- package/src/tenant/__tests__/multi-tenant.integration.test.ts +1 -1
- package/src/tenant/handlers/active-tenant-ids.query.ts +8 -2
- package/src/tenant/handlers/invitations.query.ts +13 -15
- package/src/tenant/handlers/members.query.ts +12 -12
- package/src/tenant/seeding.ts +110 -16
- package/src/tenant-lifecycle/boot-checks.ts +11 -4
- package/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +4 -6
- package/src/tenant-settings/__tests__/tenant-settings.integration.test.ts +115 -0
- package/src/tenant-settings/config.ts +44 -0
- package/src/tenant-settings/constants.ts +16 -0
- package/src/tenant-settings/feature.ts +49 -0
- package/src/tenant-settings/index.ts +8 -0
- package/src/tenant-settings/tenant-defaults.ts +67 -0
- package/src/user/handlers/find-for-auth.query.ts +1 -1
- package/src/user/seeding.ts +8 -0
- package/src/user-data-rights/COMPLIANCE.md +1 -1
- package/src/user-data-rights/__tests__/audit-log.integration.test.ts +65 -0
- package/src/user-data-rights/__tests__/boot-checks.test.ts +15 -17
- package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +9 -5
- package/src/user-data-rights/__tests__/download.integration.test.ts +12 -41
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +7 -10
- package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +15 -1
- package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +120 -0
- package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +9 -4
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +74 -8
- package/src/user-data-rights/db/queries/__tests__/forget-cleanup.integration.test.ts +69 -0
- package/src/user-data-rights/db/queries/forget-cleanup.ts +9 -2
- package/src/user-data-rights/feature.ts +45 -45
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +8 -11
- package/src/user-data-rights/handlers/download-by-job.query.ts +6 -6
- package/src/user-data-rights/handlers/download-by-token.query.ts +6 -6
- package/src/user-data-rights/handlers/lift-restriction.write.ts +33 -2
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -0
- package/src/user-data-rights/handlers/my-audit-log.query.ts +1 -0
- package/src/user-data-rights/handlers/restrict-account.write.ts +33 -4
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +2 -0
- package/src/user-data-rights/lib/is-admin-actor.ts +19 -0
- package/src/user-data-rights/run-forget-cleanup.ts +27 -7
|
@@ -3,12 +3,16 @@ import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import { RequestAccountUnlockScreen } from "../request-account-unlock-screen";
|
|
4
4
|
import { renderWithProviders } from "./test-utils";
|
|
5
5
|
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
6
8
|
beforeEach(() => {
|
|
7
9
|
globalThis.fetch = mock(
|
|
8
10
|
async () => new Response(null, { status: 200 }),
|
|
9
11
|
) as unknown as typeof fetch;
|
|
10
12
|
});
|
|
11
|
-
afterEach(() => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = originalFetch;
|
|
15
|
+
});
|
|
12
16
|
|
|
13
17
|
describe("RequestAccountUnlockScreen", () => {
|
|
14
18
|
test("renders title + email input + submit (de)", () => {
|
|
@@ -33,6 +37,8 @@ describe("RequestAccountUnlockScreen", () => {
|
|
|
33
37
|
"/api/auth/request-account-unlock",
|
|
34
38
|
expect.objectContaining({
|
|
35
39
|
method: "POST",
|
|
40
|
+
headers: expect.objectContaining({ "Content-Type": "application/json" }),
|
|
41
|
+
credentials: "same-origin",
|
|
36
42
|
body: JSON.stringify({ email: "user@example.com" }),
|
|
37
43
|
}),
|
|
38
44
|
);
|
|
@@ -54,7 +60,7 @@ describe("RequestAccountUnlockScreen", () => {
|
|
|
54
60
|
);
|
|
55
61
|
});
|
|
56
62
|
|
|
57
|
-
test("429 with retryAfterSeconds →
|
|
63
|
+
test("429 with retryAfterSeconds → requestUnlock.rateLimited banner (#1333)", async () => {
|
|
58
64
|
globalThis.fetch = mock(
|
|
59
65
|
async () =>
|
|
60
66
|
new Response(JSON.stringify({ error: { details: { retryAfterSeconds: 180 } } }), {
|
|
@@ -3,12 +3,16 @@ import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import { ResetPasswordScreen } from "../reset-password-screen";
|
|
4
4
|
import { renderWithProviders } from "./test-utils";
|
|
5
5
|
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
6
8
|
beforeEach(() => {
|
|
7
9
|
globalThis.fetch = mock(
|
|
8
10
|
async () => new Response(null, { status: 200 }),
|
|
9
11
|
) as unknown as typeof fetch;
|
|
10
12
|
});
|
|
11
|
-
afterEach(() => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = originalFetch;
|
|
15
|
+
});
|
|
12
16
|
|
|
13
17
|
describe("ResetPasswordScreen", () => {
|
|
14
18
|
test("ohne Token in URL UND ohne token-Prop → missing-token-Page", () => {
|
|
@@ -3,12 +3,16 @@ import { screen, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import { VerifyEmailScreen } from "../verify-email-screen";
|
|
4
4
|
import { renderWithProviders } from "./test-utils";
|
|
5
5
|
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
6
8
|
beforeEach(() => {
|
|
7
9
|
globalThis.fetch = mock(
|
|
8
10
|
async () => new Response(null, { status: 200 }),
|
|
9
11
|
) as unknown as typeof fetch;
|
|
10
12
|
});
|
|
11
|
-
afterEach(() => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = originalFetch;
|
|
15
|
+
});
|
|
12
16
|
|
|
13
17
|
describe("VerifyEmailScreen", () => {
|
|
14
18
|
test("ohne Token → missing-token-Page", () => {
|
|
@@ -34,8 +34,24 @@ export type LoginResponse = {
|
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
// Reason codes the login-handler is known to emit today (login-screen.tsx's
|
|
38
|
+
// reasonToKey maps every one of these to an i18n key). The server can also
|
|
39
|
+
// send a reason this client doesn't know about yet (rolling deploys, a new
|
|
40
|
+
// enforcement policy) — `string & {}` keeps that open while still giving
|
|
41
|
+
// autocomplete/typo-checking on the known set, instead of collapsing both
|
|
42
|
+
// to plain `string` (coding-standards: no magic strings without a union).
|
|
43
|
+
export type KnownLoginFailureReason =
|
|
44
|
+
| "invalid_credentials"
|
|
45
|
+
| "no_membership"
|
|
46
|
+
| "account_locked"
|
|
47
|
+
| "email_not_verified"
|
|
48
|
+
| "rate_limited"
|
|
49
|
+
| "invalid_body"
|
|
50
|
+
| "mfa_not_supported"
|
|
51
|
+
| "mfa_setup_required";
|
|
52
|
+
|
|
37
53
|
export type LoginFailure = {
|
|
38
|
-
readonly reason: string;
|
|
54
|
+
readonly reason: KnownLoginFailureReason | (string & {});
|
|
39
55
|
readonly message?: string;
|
|
40
56
|
readonly retryAfterSeconds?: number;
|
|
41
57
|
};
|
|
@@ -93,8 +109,14 @@ export async function login(req: LoginRequest): Promise<LoginResult> {
|
|
|
93
109
|
if (body.mfaRequired === true && typeof body.challengeToken === "string") {
|
|
94
110
|
return { kind: "mfa-challenge", challengeToken: body.challengeToken };
|
|
95
111
|
}
|
|
96
|
-
if (body.mfaSetupRequired === true
|
|
97
|
-
|
|
112
|
+
if (body.mfaSetupRequired === true) {
|
|
113
|
+
// Older servers / mounts may set the flag without minting a preauth token —
|
|
114
|
+
// surface an explicit failure (login-screen maps mfa_setup_required) instead
|
|
115
|
+
// of falling through to generic login_failed.
|
|
116
|
+
if (typeof body.preauthSetupToken === "string") {
|
|
117
|
+
return { kind: "mfa-setup-required", preauthSetupToken: body.preauthSetupToken };
|
|
118
|
+
}
|
|
119
|
+
return { kind: "failure", error: { reason: "mfa_setup_required" } };
|
|
98
120
|
}
|
|
99
121
|
if (body.token !== undefined && body.user !== undefined) {
|
|
100
122
|
return { kind: "success", data: { token: body.token, user: body.user } };
|
|
@@ -167,10 +189,10 @@ async function parseTokenFailure(res: Response): Promise<AuthTokenFailure> {
|
|
|
167
189
|
return { reason };
|
|
168
190
|
}
|
|
169
191
|
|
|
170
|
-
// POST /api/auth/request-password-reset. 200 silent-success:
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
192
|
+
// POST /api/auth/request-password-reset. 200 silent-success: even if the
|
|
193
|
+
// email doesn't exist, the caller sees `{ ok: true }` — no account
|
|
194
|
+
// enumeration. Server only triggers the mail internally when a user is
|
|
195
|
+
// found. 429 → typed rate-limit failure. 5xx → unknown-error.
|
|
174
196
|
export async function requestPasswordReset(
|
|
175
197
|
email: string,
|
|
176
198
|
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
@@ -204,7 +226,7 @@ export async function resetPassword(
|
|
|
204
226
|
}
|
|
205
227
|
|
|
206
228
|
// POST /api/auth/request-email-verification. Same silent-success
|
|
207
|
-
//
|
|
229
|
+
// semantics as request-password-reset. 429 → rate-limit failure.
|
|
208
230
|
export async function requestEmailVerification(
|
|
209
231
|
email: string,
|
|
210
232
|
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
@@ -234,8 +256,8 @@ export async function verifyEmail(
|
|
|
234
256
|
return { ok: false, error: await parseTokenFailure(res) };
|
|
235
257
|
}
|
|
236
258
|
|
|
237
|
-
// POST /api/auth/request-account-unlock. Same silent-success
|
|
238
|
-
// request-password-reset. 429 → rate-limit
|
|
259
|
+
// POST /api/auth/request-account-unlock. Same silent-success semantics as
|
|
260
|
+
// request-password-reset. 429 → rate-limit failure.
|
|
239
261
|
export async function requestAccountUnlock(
|
|
240
262
|
email: string,
|
|
241
263
|
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
@@ -109,10 +109,17 @@ export function createLoginRoute(
|
|
|
109
109
|
preauthSetupToken={setupRequest.preauthSetupToken}
|
|
110
110
|
accountLabel={setupRequest.accountLabel}
|
|
111
111
|
onSuccess={() => {
|
|
112
|
-
setSetupRequest(null);
|
|
113
112
|
// MfaSetupPreauthScreen has no session to refresh itself with
|
|
114
113
|
// (it runs pre-auth) — the gate owns the session, so it refreshes.
|
|
115
|
-
|
|
114
|
+
// Keep setupRequest until refresh settles: clearing first would
|
|
115
|
+
// dump the user back on the login form with no retry if refresh
|
|
116
|
+
// rejects (cookie+JWT already minted server-side).
|
|
117
|
+
void refresh().then(
|
|
118
|
+
() => setSetupRequest(null),
|
|
119
|
+
() => {
|
|
120
|
+
// leave setupRequest set so the user can cancel/retry
|
|
121
|
+
},
|
|
122
|
+
);
|
|
116
123
|
}}
|
|
117
124
|
onCancel={() => setSetupRequest(null)}
|
|
118
125
|
/>
|
|
@@ -136,18 +143,10 @@ export function createLoginRoute(
|
|
|
136
143
|
return LoginRoute;
|
|
137
144
|
}
|
|
138
145
|
|
|
139
|
-
export function makeAuthGate(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
MfaSetupComponent?: ComponentType<MfaSetupComponentProps>,
|
|
144
|
-
): ComponentType<{ children: ReactNode }> {
|
|
145
|
-
const LoginRoute = createLoginRoute({
|
|
146
|
-
loginScreen: LoginComponent,
|
|
147
|
-
loginScreenProps: loginProps,
|
|
148
|
-
mfaVerifyScreen: MfaVerifyComponent,
|
|
149
|
-
mfaSetupScreen: MfaSetupComponent,
|
|
150
|
-
});
|
|
146
|
+
export function makeAuthGate(opts: LoginRouteOptions = {}): ComponentType<{
|
|
147
|
+
children: ReactNode;
|
|
148
|
+
}> {
|
|
149
|
+
const LoginRoute = createLoginRoute(opts);
|
|
151
150
|
function AuthGate({ children }: { readonly children: ReactNode }): ReactNode {
|
|
152
151
|
const { status } = useSession();
|
|
153
152
|
if (status === "authenticated") return <>{children}</>;
|
|
@@ -159,13 +158,10 @@ export function makeAuthGate(
|
|
|
159
158
|
// SessionProvider + AuthGate as one gate, so a public gate in front (e.g.
|
|
160
159
|
// /calculator) doesn't mount the session bootstrap — createKumikoApp stacks
|
|
161
160
|
// providers outside all gates, so SessionProvider can't be a provider anymore.
|
|
162
|
-
export function makeSessionAuthGate(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
MfaSetupComponent?: ComponentType<MfaSetupComponentProps>,
|
|
167
|
-
): ComponentType<{ children: ReactNode }> {
|
|
168
|
-
const AuthGate = makeAuthGate(LoginComponent, loginProps, MfaVerifyComponent, MfaSetupComponent);
|
|
161
|
+
export function makeSessionAuthGate(opts: LoginRouteOptions = {}): ComponentType<{
|
|
162
|
+
children: ReactNode;
|
|
163
|
+
}> {
|
|
164
|
+
const AuthGate = makeAuthGate(opts);
|
|
169
165
|
function SessionAuthGate({ children }: { readonly children: ReactNode }): ReactNode {
|
|
170
166
|
return (
|
|
171
167
|
<SessionProvider>
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
9
9
|
import type { ComponentType, ReactNode } from "react";
|
|
10
10
|
import { defaultTranslations, mergeTranslations } from "../i18n";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type MfaSetupComponentProps,
|
|
13
|
+
type MfaVerifyComponentProps,
|
|
14
|
+
makeSessionAuthGate,
|
|
15
|
+
} from "./auth-gate";
|
|
12
16
|
import type { LoginScreenProps } from "./login-screen";
|
|
13
17
|
|
|
14
18
|
export type EmailPasswordClientOptions = {
|
|
@@ -24,6 +28,10 @@ export type EmailPasswordClientOptions = {
|
|
|
24
28
|
* auth-mfa simply don't pass this — LoginScreen's built-in fallback
|
|
25
29
|
* error covers that case. */
|
|
26
30
|
readonly mfaVerifyScreen?: ComponentType<MfaVerifyComponentProps>;
|
|
31
|
+
/** auth-mfa's MfaSetupPreauthScreen, wired in from the app the same way
|
|
32
|
+
* as mfaVerifyScreen above. Apps not mounting auth-mfa simply don't
|
|
33
|
+
* pass this. */
|
|
34
|
+
readonly mfaSetupScreen?: ComponentType<MfaSetupComponentProps>;
|
|
27
35
|
/** Key-Overrides pro Locale. Wird mit den Default-Bundles (de/en)
|
|
28
36
|
* aus `translations.ts` gemerged — jeder hier gesetzte Key gewinnt.
|
|
29
37
|
* Für Branding ("Sign in" → "Login to Acme") oder weitere Sprachen
|
|
@@ -49,7 +57,12 @@ export function emailPasswordClient(
|
|
|
49
57
|
name: "auth-email-password",
|
|
50
58
|
providers: [],
|
|
51
59
|
gates: [
|
|
52
|
-
makeSessionAuthGate(
|
|
60
|
+
makeSessionAuthGate({
|
|
61
|
+
loginScreen: options.loginScreen,
|
|
62
|
+
loginScreenProps: options.loginScreenProps,
|
|
63
|
+
mfaVerifyScreen: options.mfaVerifyScreen,
|
|
64
|
+
mfaSetupScreen: options.mfaSetupScreen,
|
|
65
|
+
}),
|
|
53
66
|
],
|
|
54
67
|
translations,
|
|
55
68
|
};
|
|
@@ -29,7 +29,11 @@ export {
|
|
|
29
29
|
} from "./auth-client";
|
|
30
30
|
export type { AuthCardProps, AuthShellRenderer } from "./auth-form-primitives";
|
|
31
31
|
export { AuthCard, AuthShellProvider, useAuthShell } from "./auth-form-primitives";
|
|
32
|
-
export type {
|
|
32
|
+
export type {
|
|
33
|
+
LoginRouteOptions,
|
|
34
|
+
MfaSetupComponentProps,
|
|
35
|
+
MfaVerifyComponentProps,
|
|
36
|
+
} from "./auth-gate";
|
|
33
37
|
export { createLoginRoute, makeAuthGate, makeSessionAuthGate } from "./auth-gate";
|
|
34
38
|
export type {
|
|
35
39
|
EmailPasswordClientFeature,
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
13
13
|
import { type FormEvent, type ReactNode, useState } from "react";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
type KnownLoginFailureReason,
|
|
16
|
+
type LoginFailure,
|
|
17
|
+
requestEmailVerification,
|
|
18
|
+
} from "./auth-client";
|
|
15
19
|
import { AuthCard } from "./auth-form-primitives";
|
|
16
20
|
import { useSession } from "./session";
|
|
17
21
|
|
|
@@ -144,7 +148,7 @@ export function LoginScreen({
|
|
|
144
148
|
onMfaChallenge(res.challengeToken);
|
|
145
149
|
return;
|
|
146
150
|
}
|
|
147
|
-
setError({ reason: "mfa_not_supported" });
|
|
151
|
+
setError({ reason: "mfa_not_supported" satisfies KnownLoginFailureReason });
|
|
148
152
|
setFailedLoginEmail(email);
|
|
149
153
|
return;
|
|
150
154
|
}
|
|
@@ -153,7 +157,7 @@ export function LoginScreen({
|
|
|
153
157
|
onMfaSetupRequired(res.preauthSetupToken, email);
|
|
154
158
|
return;
|
|
155
159
|
}
|
|
156
|
-
setError({ reason: "mfa_setup_required" });
|
|
160
|
+
setError({ reason: "mfa_setup_required" satisfies KnownLoginFailureReason });
|
|
157
161
|
setFailedLoginEmail(email);
|
|
158
162
|
return;
|
|
159
163
|
}
|
|
@@ -44,11 +44,9 @@ describe("resolveSessionStore — finds the single registered provider", () => {
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
describe("validateSessionStoreMultiplicity", () => {
|
|
47
|
-
test("0 providers registered →
|
|
47
|
+
test("0 providers registered → does not throw (valid: PAT-only / machine-API deployments mount no sessionStore)", () => {
|
|
48
48
|
const registry = createRegistry([authFoundationFeature]);
|
|
49
|
-
expect(() => validateSessionStoreMultiplicity([...registry.features.values()])).toThrow(
|
|
50
|
-
/no sessionStore provider registered/,
|
|
51
|
-
);
|
|
49
|
+
expect(() => validateSessionStoreMultiplicity([...registry.features.values()])).not.toThrow();
|
|
52
50
|
});
|
|
53
51
|
|
|
54
52
|
test("exactly 1 provider → no throw", () => {
|
|
@@ -134,3 +134,27 @@ describe("resolveAnonymousAccessFromRegistry — fail-closed trust", () => {
|
|
|
134
134
|
).rejects.toThrow(/resolverTrust/);
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
|
+
|
|
138
|
+
describe("resolveAnonymousAccessFromRegistry — opt-in only (#1452)", () => {
|
|
139
|
+
test("a mounted tenantResolver provider alone does not make anonymousAccess defined", async () => {
|
|
140
|
+
const registry = createRegistry([authFoundationFeature, resolverProvider("subdomain")]);
|
|
141
|
+
const result = await resolveAnonymousAccessFromRegistry(undefined, { db: fakeDb, registry });
|
|
142
|
+
expect(result).toBeUndefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("a mounted tenantExists provider alone does not make anonymousAccess defined", async () => {
|
|
146
|
+
const registry = createRegistry([authFoundationFeature, existenceProvider("db")]);
|
|
147
|
+
const result = await resolveAnonymousAccessFromRegistry(undefined, { db: fakeDb, registry });
|
|
148
|
+
expect(result).toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("an app that explicitly opts in still gets providers merged", async () => {
|
|
152
|
+
const registry = createRegistry([authFoundationFeature, resolverProvider("subdomain")]);
|
|
153
|
+
const result = await resolveAnonymousAccessFromRegistry(
|
|
154
|
+
{ defaultTenantId: undefined },
|
|
155
|
+
{ db: fakeDb, registry },
|
|
156
|
+
);
|
|
157
|
+
expect(result?.tenantResolver).toBeDefined();
|
|
158
|
+
expect(result?.resolverTrust).toBe("authoritative");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -8,7 +8,13 @@ import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
8
8
|
import { createRegistry, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
9
|
import { validateTokenVerifierMultiplicity } from "../boot-checks";
|
|
10
10
|
import { authFoundationFeature, resolveTokenVerifier } from "../feature";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type AuthProviderPlugin,
|
|
13
|
+
EXT_SESSION_STORE,
|
|
14
|
+
EXT_TOKEN_VERIFIER,
|
|
15
|
+
type SessionStore,
|
|
16
|
+
type SessionStoreProvider,
|
|
17
|
+
} from "../types";
|
|
12
18
|
|
|
13
19
|
// Mock providers never read `deps.db` — a real provider (personal-access-
|
|
14
20
|
// tokens/resolver.ts) does a point-read + live-role-resolution against it.
|
|
@@ -38,6 +44,23 @@ function jwtProvider(entityName = "jwt-mock"): ReturnType<typeof defineFeature>
|
|
|
38
44
|
});
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
function mockStore(): SessionStore {
|
|
48
|
+
return {
|
|
49
|
+
creator: async () => "sid-1",
|
|
50
|
+
revoker: async () => undefined,
|
|
51
|
+
checker: async () => "live",
|
|
52
|
+
massRevoker: async () => 0,
|
|
53
|
+
revokeAllOthers: async () => 0,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function storeProvider(entityName = "mock-store"): ReturnType<typeof defineFeature> {
|
|
58
|
+
const plugin: SessionStoreProvider = { build: () => mockStore() };
|
|
59
|
+
return defineFeature(`mock-${entityName}`, (r) => {
|
|
60
|
+
r.useExtension(EXT_SESSION_STORE, entityName, plugin);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
describe("resolveTokenVerifier — middleware finds the matching provider", () => {
|
|
42
65
|
test("routes a prefixed token to the prefix-shape provider", async () => {
|
|
43
66
|
const registry = createRegistry([authFoundationFeature, prefixProvider("pat-mock", "kpat_")]);
|
|
@@ -65,13 +88,23 @@ describe("resolveTokenVerifier — middleware finds the matching provider", () =
|
|
|
65
88
|
});
|
|
66
89
|
|
|
67
90
|
describe("validateTokenVerifierMultiplicity", () => {
|
|
68
|
-
test("0
|
|
91
|
+
test("0 tokenVerifier + 0 sessionStore → throws (neither auth path mounted)", () => {
|
|
69
92
|
const registry = createRegistry([authFoundationFeature]);
|
|
70
93
|
expect(() => validateTokenVerifierMultiplicity([...registry.features.values()])).toThrow(
|
|
71
|
-
/no tokenVerifier providers registered/,
|
|
94
|
+
/no tokenVerifier providers and no sessionStore registered/,
|
|
72
95
|
);
|
|
73
96
|
});
|
|
74
97
|
|
|
98
|
+
test("0 tokenVerifier + sessionStore → no throw (session-only / #1570)", () => {
|
|
99
|
+
const registry = createRegistry([authFoundationFeature, storeProvider()]);
|
|
100
|
+
expect(() => validateTokenVerifierMultiplicity([...registry.features.values()])).not.toThrow();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("tokenVerifier alone (PAT-only) → no throw", () => {
|
|
104
|
+
const registry = createRegistry([authFoundationFeature, prefixProvider("pat-mock", "kpat_")]);
|
|
105
|
+
expect(() => validateTokenVerifierMultiplicity([...registry.features.values()])).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
|
|
75
108
|
test("1 provider per shape → no throw", () => {
|
|
76
109
|
const registry = createRegistry([
|
|
77
110
|
authFoundationFeature,
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// Multiplicity boot-check for auth-foundation's `tokenVerifier` extension
|
|
2
|
-
// point (#1368). Two static-shape conflicts a boot-check CAN catch (a
|
|
2
|
+
// point (#1368, #1570). Two static-shape conflicts a boot-check CAN catch (a
|
|
3
3
|
// runtime shape-match predicate's overlap can't be proven at boot): a
|
|
4
4
|
// malformed plugin registration, and two providers both claiming the same
|
|
5
|
-
// shape (resolveTokenVerifier can't tell them apart). Zero
|
|
6
|
-
// providers is
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// which
|
|
11
|
-
//
|
|
5
|
+
// shape (resolveTokenVerifier can't tell them apart). Zero tokenVerifier
|
|
6
|
+
// providers is OK when ≥1 sessionStore is mounted (session-only cookie
|
|
7
|
+
// apps never use Bearer routing). Zero of both is caught here — fail-fast
|
|
8
|
+
// at boot ("you mounted the foundation but forgot sessions or a provider")
|
|
9
|
+
// rather than a runtime 401 nobody can attribute, unlike
|
|
10
|
+
// file-foundation/mail-foundation which defer that check to request-time
|
|
11
|
+
// (their provider is picked from tenant config, which doesn't exist at
|
|
12
|
+
// boot; auth-foundation's providers are static per-deployment, so boot is
|
|
13
|
+
// the right time to catch it).
|
|
12
14
|
|
|
13
15
|
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
16
|
import {
|
|
@@ -43,9 +45,18 @@ export function validateTokenVerifierMultiplicity(features: readonly FeatureDefi
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
if (namesByShape.size === 0) {
|
|
48
|
+
// Session-only apps (email-password + sessions) never hit tokenVerifier —
|
|
49
|
+
// cookie JWT auth uses sessionStore alone (#1570 / phronexsis#296).
|
|
50
|
+
for (const feature of features) {
|
|
51
|
+
for (const usage of feature.extensionUsages) {
|
|
52
|
+
// skip: sessionStore present — tokenVerifier optional for cookie-session apps.
|
|
53
|
+
if (usage.extensionName === EXT_SESSION_STORE) return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
46
56
|
throw new Error(
|
|
47
|
-
"[auth-foundation] no tokenVerifier providers registered — mount
|
|
48
|
-
"auth-provider-* feature (e.g. auth-provider-jwt
|
|
57
|
+
"[auth-foundation] no tokenVerifier providers and no sessionStore registered — mount " +
|
|
58
|
+
"at least one auth-provider-* feature (e.g. auth-provider-jwt / personal-access-tokens) " +
|
|
59
|
+
"or a sessionStore (e.g. sessions) alongside auth-foundation.",
|
|
49
60
|
);
|
|
50
61
|
}
|
|
51
62
|
|
|
@@ -60,86 +71,80 @@ export function validateTokenVerifierMultiplicity(features: readonly FeatureDefi
|
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
74
|
+
// Shared by the three single-provider extension points below: collect every
|
|
75
|
+
// registration for `ext`, validating each usage's options against `guard`,
|
|
76
|
+
// then fail boot on ≥2 (resolve*() has no way to route between them). Zero
|
|
77
|
+
// registrations is NOT checked here — it's valid for all three (a
|
|
78
|
+
// machine-API-only auth-foundation with PAT-bearer auth and no browser
|
|
79
|
+
// sessions, single-tenant deployments with no tenantResolver, etc.); the
|
|
80
|
+
// runtime already models "extension point unmounted" as a legal
|
|
81
|
+
// configuration (run-prod-app.ts only wires sessions when
|
|
82
|
+
// EXT_SESSION_STORE has ≥1 usage), and resolve*() throws its own clear
|
|
83
|
+
// error if a caller requests a provider that was never registered.
|
|
84
|
+
function validateSingleProvider(
|
|
85
|
+
features: readonly FeatureDefinition[],
|
|
86
|
+
opts: {
|
|
87
|
+
readonly ext: string;
|
|
88
|
+
readonly label: string;
|
|
89
|
+
readonly guard: (options: unknown) => boolean;
|
|
90
|
+
readonly shapeHint: string;
|
|
91
|
+
},
|
|
92
|
+
): void {
|
|
93
|
+
const { ext, label, guard, shapeHint } = opts;
|
|
67
94
|
const names: string[] = [];
|
|
68
95
|
|
|
69
96
|
for (const feature of features) {
|
|
70
97
|
for (const usage of feature.extensionUsages) {
|
|
71
|
-
if (usage.extensionName !==
|
|
72
|
-
if (!
|
|
98
|
+
if (usage.extensionName !== ext) continue;
|
|
99
|
+
if (!guard(usage.options)) {
|
|
73
100
|
throw new Error(
|
|
74
|
-
`[auth-foundation]
|
|
75
|
-
`registered without a valid
|
|
101
|
+
`[auth-foundation] ${label} provider "${usage.entityName}" (feature "${feature.name}") ` +
|
|
102
|
+
`registered without a valid ${shapeHint}.`,
|
|
76
103
|
);
|
|
77
104
|
}
|
|
78
105
|
names.push(usage.entityName);
|
|
79
106
|
}
|
|
80
107
|
}
|
|
81
108
|
|
|
82
|
-
if (names.length === 0) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
"[auth-foundation] no sessionStore provider registered — mount a feature that " +
|
|
85
|
-
"registers one via r.useExtension(EXT_SESSION_STORE, ...) alongside auth-foundation.",
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
109
|
if (names.length >= 2) {
|
|
90
110
|
throw new Error(
|
|
91
|
-
`[auth-foundation] ${names.length}
|
|
92
|
-
`only one
|
|
111
|
+
`[auth-foundation] ${names.length} ${label} providers registered (${names.join(", ")}) — ` +
|
|
112
|
+
`only one ${label} provider may be mounted at a time.`,
|
|
93
113
|
);
|
|
94
114
|
}
|
|
95
115
|
}
|
|
96
116
|
|
|
117
|
+
// Multiplicity boot-check for the `sessionStore` extension point (#1370).
|
|
118
|
+
// Single-provider, unlike tokenVerifier — no shape to route on, so ≥2 fails
|
|
119
|
+
// boot. Zero registrations is a valid configuration (see
|
|
120
|
+
// validateSingleProvider doc) — a pure machine API using auth-foundation for
|
|
121
|
+
// PAT-bearer auth alone can mount it without also pulling in the whole
|
|
122
|
+
// sessions feature.
|
|
123
|
+
export function validateSessionStoreMultiplicity(features: readonly FeatureDefinition[]): void {
|
|
124
|
+
validateSingleProvider(features, {
|
|
125
|
+
ext: EXT_SESSION_STORE,
|
|
126
|
+
label: "sessionStore",
|
|
127
|
+
guard: isSessionStoreProvider,
|
|
128
|
+
shapeHint: "SessionStoreProvider — options must have a { build } shape",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
97
132
|
// Optional single-provider (#1373). Zero registrations = OK (single-tenant /
|
|
98
133
|
// header-cookie path). ≥2 or malformed = boot fail.
|
|
99
134
|
export function validateTenantResolverMultiplicity(features: readonly FeatureDefinition[]): void {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
throw new Error(
|
|
107
|
-
`[auth-foundation] tenantResolver provider "${usage.entityName}" (feature "${feature.name}") ` +
|
|
108
|
-
`registered without a valid TenantResolverProvider — options must have { trust, build }.`,
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
names.push(usage.entityName);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (names.length >= 2) {
|
|
116
|
-
throw new Error(
|
|
117
|
-
`[auth-foundation] ${names.length} tenantResolver providers registered (${names.join(", ")}) — ` +
|
|
118
|
-
`only one tenantResolver provider may be mounted at a time.`,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
135
|
+
validateSingleProvider(features, {
|
|
136
|
+
ext: EXT_TENANT_RESOLVER,
|
|
137
|
+
label: "tenantResolver",
|
|
138
|
+
guard: isTenantResolverProvider,
|
|
139
|
+
shapeHint: "TenantResolverProvider — options must have { trust, build }",
|
|
140
|
+
});
|
|
121
141
|
}
|
|
122
142
|
|
|
123
143
|
export function validateTenantExistenceMultiplicity(features: readonly FeatureDefinition[]): void {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
throw new Error(
|
|
131
|
-
`[auth-foundation] tenantExistence provider "${usage.entityName}" (feature "${feature.name}") ` +
|
|
132
|
-
`registered without a valid TenantExistenceProvider — options must have a { build } shape.`,
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
names.push(usage.entityName);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (names.length >= 2) {
|
|
140
|
-
throw new Error(
|
|
141
|
-
`[auth-foundation] ${names.length} tenantExistence providers registered (${names.join(", ")}) — ` +
|
|
142
|
-
`only one tenantExistence provider may be mounted at a time.`,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
144
|
+
validateSingleProvider(features, {
|
|
145
|
+
ext: EXT_TENANT_EXISTENCE,
|
|
146
|
+
label: "tenantExistence",
|
|
147
|
+
guard: isTenantExistenceProvider,
|
|
148
|
+
shapeHint: "TenantExistenceProvider — options must have a { build } shape",
|
|
149
|
+
});
|
|
145
150
|
}
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
import type {
|
|
7
7
|
AnonymousAccessConfig,
|
|
8
8
|
AnonymousAccessResolved,
|
|
9
|
-
TenantExists,
|
|
10
|
-
TenantResolver,
|
|
11
9
|
} from "@cosmicdrift/kumiko-framework/api";
|
|
12
10
|
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
11
|
import { resolveTenantExistence, resolveTenantResolver } from "./feature";
|
|
@@ -22,7 +20,12 @@ export async function resolveAnonymousAccessFromRegistry(
|
|
|
22
20
|
resolveTenantExistence(deps),
|
|
23
21
|
]);
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
// Anonymous access is app opt-in only: a mounted routing provider
|
|
24
|
+
// (EXT_TENANT_RESOLVER/EXT_TENANT_EXISTENCE) alone must never make
|
|
25
|
+
// anonymousAccess become defined for an app that never set it — that
|
|
26
|
+
// would upgrade every roles:["anonymous"] handler to reachable without
|
|
27
|
+
// a JWT the moment such a routing feature is mounted (#1452).
|
|
28
|
+
if (!base) return undefined;
|
|
26
29
|
|
|
27
30
|
// Fail-closed (#1374 security): inline tenantResolver without trust is the
|
|
28
31
|
// old ambiguous config — providers always carry trust; test stacks must too.
|
|
@@ -45,10 +48,15 @@ export async function resolveAnonymousAccessFromRegistry(
|
|
|
45
48
|
...(base ?? {}),
|
|
46
49
|
...(tenantResolver
|
|
47
50
|
? {
|
|
48
|
-
|
|
51
|
+
// No cast: TenantResolverFn's `(c: unknown) => string | null`
|
|
52
|
+
// and the framework's TenantResolver `(c: Context) => TenantId
|
|
53
|
+
// | null` are structurally identical (TenantId is a plain
|
|
54
|
+
// string alias, no brand) — `unknown` is a supertype of
|
|
55
|
+
// Context, so this is already sound by parameter contravariance.
|
|
56
|
+
tenantResolver: tenantResolver.resolve,
|
|
49
57
|
resolverTrust: tenantResolver.trust,
|
|
50
58
|
}
|
|
51
59
|
: {}),
|
|
52
|
-
...(tenantExists ? { tenantExists
|
|
60
|
+
...(tenantExists ? { tenantExists } : {}),
|
|
53
61
|
};
|
|
54
62
|
}
|