@cosmicdrift/kumiko-bundled-features 0.160.0 → 0.162.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 +6 -6
- package/src/auth-email-password/__tests__/login-mfa-setup-required.integration.test.ts +129 -0
- package/src/auth-email-password/__tests__/self-registration-toggle.integration.test.ts +168 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +5 -0
- package/src/auth-email-password/constants.ts +6 -0
- package/src/auth-email-password/feature.ts +8 -1
- package/src/auth-email-password/handlers/login.write.ts +6 -3
- package/src/auth-email-password/handlers/self-registration-status.query.ts +20 -0
- package/src/auth-email-password/handlers/signup-request.write.ts +11 -0
- package/src/auth-email-password/i18n.ts +4 -0
- package/src/auth-email-password/index.ts +5 -1
- package/src/auth-email-password/self-registration-toggle.ts +25 -0
- package/src/auth-email-password/web/__tests__/auth-client.test.ts +6 -2
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +79 -0
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +14 -5
- package/src/auth-email-password/web/auth-client.ts +4 -3
- package/src/auth-email-password/web/auth-gate.tsx +44 -2
- package/src/auth-email-password/web/login-screen.tsx +11 -5
- package/src/auth-foundation/__tests__/feature.test.ts +16 -3
- package/src/auth-foundation/__tests__/session-store.test.ts +1 -0
- package/src/auth-foundation/__tests__/tenant-providers.test.ts +136 -0
- package/src/auth-foundation/boot-checks.ts +54 -0
- package/src/auth-foundation/feature.ts +77 -23
- package/src/auth-foundation/index.ts +14 -0
- package/src/auth-foundation/resolve-anonymous-access.ts +54 -0
- package/src/auth-foundation/types.ts +63 -0
- package/src/auth-mfa/__tests__/enable-confirm-preauth.integration.test.ts +253 -0
- package/src/auth-mfa/__tests__/enable-start-preauth.integration.test.ts +214 -0
- package/src/auth-mfa/__tests__/mfa-preauth-setup-token.test.ts +87 -0
- package/src/auth-mfa/constants.ts +3 -0
- package/src/auth-mfa/feature.ts +15 -0
- package/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +170 -0
- package/src/auth-mfa/handlers/enable-start-preauth.write.ts +87 -0
- package/src/auth-mfa/mfa-preauth-setup-token.ts +86 -0
- package/src/auth-mfa/mfa-setup-token.ts +7 -1
- package/src/auth-mfa/mfa-status-checker.ts +13 -4
- package/src/auth-mfa/web/__tests__/mfa-client.test.ts +144 -1
- package/src/auth-mfa/web/__tests__/mfa-setup-preauth-screen.test.tsx +202 -0
- package/src/auth-mfa/web/i18n.ts +14 -0
- package/src/auth-mfa/web/index.ts +8 -2
- package/src/auth-mfa/web/mfa-client.ts +113 -0
- package/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +244 -0
- package/src/feature-toggles/__tests__/compose-tier-resolver.test.ts +96 -0
- package/src/feature-toggles/compose-tier-resolver.ts +41 -0
- package/src/feature-toggles/index.ts +1 -0
- package/src/feature-toggles/toggle-runtime.ts +14 -0
- package/src/sessions/feature.ts +8 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Unit test (no DB) — the composition logic is pure given a tier resolver
|
|
2
|
+
// function + a GlobalFeatureToggleRuntime whose in-memory snapshot is set
|
|
3
|
+
// directly via .apply(). Pins the semantics documented in
|
|
4
|
+
// compose-tier-resolver.ts: the global layer only NARROWS what the tier
|
|
5
|
+
// grants (explicit `false` removes it), it never falls back to a
|
|
6
|
+
// toggleable feature's own default and never widens beyond the tier.
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
10
|
+
import type {
|
|
11
|
+
EffectiveFeaturesResolver,
|
|
12
|
+
Registry,
|
|
13
|
+
TenantId,
|
|
14
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
15
|
+
import { composeTierResolverWithGlobalToggles } from "../compose-tier-resolver";
|
|
16
|
+
import { GlobalFeatureToggleRuntime } from "../toggle-runtime";
|
|
17
|
+
|
|
18
|
+
// Never touched by .apply()/.readOverride() — the runtime only does I/O in
|
|
19
|
+
// .initialize()/.refresh(), which this test never calls.
|
|
20
|
+
const fakeDb = {} as unknown as DbConnection;
|
|
21
|
+
const fakeRegistry = {} as unknown as Registry;
|
|
22
|
+
|
|
23
|
+
function tierResolverGranting(...features: readonly string[]): EffectiveFeaturesResolver {
|
|
24
|
+
return ((_tenantId: TenantId) => new Set(features)) as EffectiveFeaturesResolver;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("composeTierResolverWithGlobalToggles", () => {
|
|
28
|
+
test("no override → tier's grant stands", () => {
|
|
29
|
+
const runtime = new GlobalFeatureToggleRuntime(fakeDb, fakeRegistry);
|
|
30
|
+
const composed = composeTierResolverWithGlobalToggles(
|
|
31
|
+
tierResolverGranting("auth-self-registration", "personal-access-tokens"),
|
|
32
|
+
runtime,
|
|
33
|
+
);
|
|
34
|
+
expect([...composed("t1" as TenantId)].sort()).toEqual([
|
|
35
|
+
"auth-self-registration",
|
|
36
|
+
"personal-access-tokens",
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("explicit override=false removes a tier-granted feature", () => {
|
|
41
|
+
const runtime = new GlobalFeatureToggleRuntime(fakeDb, fakeRegistry);
|
|
42
|
+
runtime.apply("auth-self-registration", false);
|
|
43
|
+
const composed = composeTierResolverWithGlobalToggles(
|
|
44
|
+
tierResolverGranting("auth-self-registration", "personal-access-tokens"),
|
|
45
|
+
runtime,
|
|
46
|
+
);
|
|
47
|
+
expect([...composed("t1" as TenantId)]).toEqual(["personal-access-tokens"]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("explicit override=true is a no-op — cannot widen beyond the tier's grant", () => {
|
|
51
|
+
const runtime = new GlobalFeatureToggleRuntime(fakeDb, fakeRegistry);
|
|
52
|
+
runtime.apply("some-other-feature", true);
|
|
53
|
+
const composed = composeTierResolverWithGlobalToggles(
|
|
54
|
+
tierResolverGranting("auth-self-registration"),
|
|
55
|
+
runtime,
|
|
56
|
+
);
|
|
57
|
+
expect([...composed("t1" as TenantId)]).toEqual(["auth-self-registration"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a tier-gated toggleable feature with NO override row stays on — no fallback to its own default", () => {
|
|
61
|
+
// Regression pin for the exact bug this composer exists to avoid:
|
|
62
|
+
// a Team-tier feature declared `toggleable({default:false})` must stay
|
|
63
|
+
// granted when the tier includes it and no operator has touched it —
|
|
64
|
+
// computeEffectiveFeatures' cascade would otherwise fall back to
|
|
65
|
+
// `false` the moment feature-toggles is composed in.
|
|
66
|
+
const runtime = new GlobalFeatureToggleRuntime(fakeDb, fakeRegistry);
|
|
67
|
+
const composed = composeTierResolverWithGlobalToggles(
|
|
68
|
+
tierResolverGranting("personal-access-tokens"),
|
|
69
|
+
runtime,
|
|
70
|
+
);
|
|
71
|
+
expect([...composed("t1" as TenantId)]).toEqual(["personal-access-tokens"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("flip back on restores the tier's grant", () => {
|
|
75
|
+
const runtime = new GlobalFeatureToggleRuntime(fakeDb, fakeRegistry);
|
|
76
|
+
runtime.apply("auth-self-registration", false);
|
|
77
|
+
const composed = composeTierResolverWithGlobalToggles(
|
|
78
|
+
tierResolverGranting("auth-self-registration"),
|
|
79
|
+
runtime,
|
|
80
|
+
);
|
|
81
|
+
expect([...composed("t1" as TenantId)]).toEqual([]);
|
|
82
|
+
|
|
83
|
+
runtime.apply("auth-self-registration", true);
|
|
84
|
+
expect([...composed("t1" as TenantId)]).toEqual(["auth-self-registration"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("preserves the tier resolver's trialGate", async () => {
|
|
88
|
+
const runtime = new GlobalFeatureToggleRuntime(fakeDb, fakeRegistry);
|
|
89
|
+
const tierResolver = tierResolverGranting();
|
|
90
|
+
const trialGate = async (_tenantId: TenantId, _featureName: string) => true;
|
|
91
|
+
Object.assign(tierResolver, { trialGate });
|
|
92
|
+
|
|
93
|
+
const composed = composeTierResolverWithGlobalToggles(tierResolver, runtime);
|
|
94
|
+
expect(composed.trialGate).toBe(trialGate);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { EffectiveFeaturesResolver } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { GlobalFeatureToggleRuntime } from "./toggle-runtime";
|
|
3
|
+
|
|
4
|
+
// Composes a tenantTierResolver (tier-engine) with a GlobalFeatureToggleRuntime
|
|
5
|
+
// (feature-toggles) into a single resolver an app can pass as
|
|
6
|
+
// runProdApp/runDevApp's `effectiveFeatures`.
|
|
7
|
+
//
|
|
8
|
+
// The framework only auto-wires ONE tenantTierResolver plugin (single-plugin
|
|
9
|
+
// assumption, see tier-resolver-extension.ts) — an app running BOTH
|
|
10
|
+
// tier-engine (per-tenant feature cuts) and feature-toggles (global runtime
|
|
11
|
+
// switches, e.g. an operator kill-switch not tied to any tier) needs to
|
|
12
|
+
// combine them itself. This is that combination, factored out here instead
|
|
13
|
+
// of duplicated per app.
|
|
14
|
+
//
|
|
15
|
+
// Semantics: the global toggle layer can only NARROW what the tier grants,
|
|
16
|
+
// never widen it — an explicit `enabled: false` row removes a feature the
|
|
17
|
+
// tier would otherwise include; no row, or an explicit `true` row, leaves
|
|
18
|
+
// the tier's grant untouched. Deliberately NOT computeEffectiveFeatures'
|
|
19
|
+
// cascade (which falls back to a toggleable feature's declared default):
|
|
20
|
+
// under tier-engine, a toggleable feature with no override row is governed
|
|
21
|
+
// by the TIER, not by its own default — falling back to the default would
|
|
22
|
+
// silently kill every tier-gated toggleable feature (e.g. a Team-tier
|
|
23
|
+
// feature defaulting `false`) the moment feature-toggles gets composed in.
|
|
24
|
+
export function composeTierResolverWithGlobalToggles(
|
|
25
|
+
tierResolver: EffectiveFeaturesResolver,
|
|
26
|
+
toggleRuntime: GlobalFeatureToggleRuntime,
|
|
27
|
+
): EffectiveFeaturesResolver {
|
|
28
|
+
const composed = ((tenantId) => {
|
|
29
|
+
const tierSet = tierResolver(tenantId);
|
|
30
|
+
const result = new Set<string>();
|
|
31
|
+
for (const name of tierSet) {
|
|
32
|
+
if (toggleRuntime.readOverride(name) !== false) result.add(name);
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}) as EffectiveFeaturesResolver;
|
|
36
|
+
|
|
37
|
+
if (tierResolver.trialGate) {
|
|
38
|
+
return Object.assign(composed, { trialGate: tierResolver.trialGate });
|
|
39
|
+
}
|
|
40
|
+
return composed;
|
|
41
|
+
}
|
|
@@ -47,6 +47,20 @@ export class GlobalFeatureToggleRuntime {
|
|
|
47
47
|
this.snapshot.set(featureName, enabled);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Raw per-feature override, bypassing the requires() cascade —
|
|
51
|
+
// `undefined` means "no explicit row, inherits toggleableDefault"
|
|
52
|
+
// (distinct from an explicit `false`). Used by composeTierResolver to
|
|
53
|
+
// layer a global kill-switch UNDER a tenantTierResolver: the composed
|
|
54
|
+
// resolver must only ever narrow what the tier already grants, never
|
|
55
|
+
// fall back to a toggleable feature's declared default the way
|
|
56
|
+
// effectiveFeatures()/computeEffectiveFeatures do — that default exists
|
|
57
|
+
// for apps with no tier resolver, and would otherwise turn tier-gated
|
|
58
|
+
// toggleable features (e.g. a Team-tier feature defaulting off) globally
|
|
59
|
+
// off the moment feature-toggles is composed in.
|
|
60
|
+
readOverride(featureName: string): boolean | undefined {
|
|
61
|
+
return this.snapshot.get(featureName);
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
// The callback shape the dispatcher expects. Computes the effective
|
|
51
65
|
// feature set from the current snapshot + the registry's requires()
|
|
52
66
|
// cascade every call. Cheap: the cascade is a DFS over O(features);
|
package/src/sessions/feature.ts
CHANGED
|
@@ -42,6 +42,9 @@ export type SessionsFeatureOptions = {
|
|
|
42
42
|
// only to supply a custom revoker — an explicit value wins over the
|
|
43
43
|
// runtime binding.
|
|
44
44
|
readonly autoRevokeOnPasswordChange?: SessionMassRevoker;
|
|
45
|
+
// Session JWT / store_user_sessions TTL. Was previously auth.sessions.expiresInMs
|
|
46
|
+
// (#1372) — lives on the sessions feature now that sessionStore is the wiring.
|
|
47
|
+
readonly expiresInMs?: number;
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
export type BindAutoRevokeOnPasswordChange = (revoker: SessionMassRevoker) => void;
|
|
@@ -120,12 +123,16 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
|
|
|
120
123
|
// via resolveSessionStore().
|
|
121
124
|
r.useExtension(EXT_SESSION_STORE, "default", {
|
|
122
125
|
build: (deps): SessionStore => {
|
|
123
|
-
const callbacks = createSessionCallbacks({
|
|
126
|
+
const callbacks = createSessionCallbacks({
|
|
127
|
+
db: deps.db,
|
|
128
|
+
...(options?.expiresInMs !== undefined && { expiresInMs: options.expiresInMs }),
|
|
129
|
+
});
|
|
124
130
|
return {
|
|
125
131
|
creator: callbacks.sessionCreator,
|
|
126
132
|
revoker: callbacks.sessionRevoker,
|
|
127
133
|
checker: callbacks.sessionChecker,
|
|
128
134
|
massRevoker: callbacks.sessionMassRevoker,
|
|
135
|
+
revokeAllOthers: callbacks.sessionRevokeAllOthers,
|
|
129
136
|
};
|
|
130
137
|
},
|
|
131
138
|
} satisfies SessionStoreProvider);
|