@cosmicdrift/kumiko-bundled-features 0.1.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 +90 -0
- package/src/audit/__tests__/audit.integration.ts +328 -0
- package/src/audit/constants.ts +7 -0
- package/src/audit/feature.ts +23 -0
- package/src/audit/handlers/list.query.ts +98 -0
- package/src/audit/index.ts +2 -0
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +149 -0
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +308 -0
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +512 -0
- package/src/auth-email-password/__tests__/auth.integration.ts +610 -0
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +67 -0
- package/src/auth-email-password/__tests__/email-templates.test.ts +106 -0
- package/src/auth-email-password/__tests__/email-verification.integration.ts +327 -0
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +174 -0
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +150 -0
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +458 -0
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +256 -0
- package/src/auth-email-password/__tests__/password-reset.integration.ts +346 -0
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +144 -0
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +176 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +310 -0
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +101 -0
- package/src/auth-email-password/__tests__/signed-token.test.ts +78 -0
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +259 -0
- package/src/auth-email-password/auth-user-row.ts +41 -0
- package/src/auth-email-password/constants.ts +101 -0
- package/src/auth-email-password/email-templates.ts +283 -0
- package/src/auth-email-password/feature.ts +140 -0
- package/src/auth-email-password/handlers/change-password.write.ts +58 -0
- package/src/auth-email-password/handlers/confirm-token-flow.ts +191 -0
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +203 -0
- package/src/auth-email-password/handlers/invite-accept.write.ts +189 -0
- package/src/auth-email-password/handlers/invite-create.write.ts +145 -0
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +192 -0
- package/src/auth-email-password/handlers/login.write.ts +208 -0
- package/src/auth-email-password/handlers/logout.write.ts +12 -0
- package/src/auth-email-password/handlers/request-email-verification.write.ts +29 -0
- package/src/auth-email-password/handlers/request-password-reset.write.ts +31 -0
- package/src/auth-email-password/handlers/reset-password.write.ts +61 -0
- package/src/auth-email-password/handlers/signup-confirm.write.ts +170 -0
- package/src/auth-email-password/handlers/signup-request.write.ts +104 -0
- package/src/auth-email-password/handlers/token-request-handler.ts +114 -0
- package/src/auth-email-password/handlers/verify-email.write.ts +62 -0
- package/src/auth-email-password/i18n.ts +211 -0
- package/src/auth-email-password/identity-v3-hash.ts +97 -0
- package/src/auth-email-password/index.ts +35 -0
- package/src/auth-email-password/invite-token-store.ts +92 -0
- package/src/auth-email-password/lockout-store.ts +118 -0
- package/src/auth-email-password/password-hashing.ts +43 -0
- package/src/auth-email-password/reset-token.ts +28 -0
- package/src/auth-email-password/seeding.ts +183 -0
- package/src/auth-email-password/signed-token.ts +85 -0
- package/src/auth-email-password/signup-token-store.ts +104 -0
- package/src/auth-email-password/stream-tenant.ts +31 -0
- package/src/auth-email-password/testing.ts +5 -0
- package/src/auth-email-password/token-burn-store.ts +57 -0
- package/src/auth-email-password/verification-token.ts +27 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +51 -0
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +80 -0
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +94 -0
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +108 -0
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +54 -0
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +100 -0
- package/src/auth-email-password/web/__tests__/test-utils.tsx +73 -0
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +55 -0
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +59 -0
- package/src/auth-email-password/web/auth-client.ts +350 -0
- package/src/auth-email-password/web/auth-form-primitives.tsx +70 -0
- package/src/auth-email-password/web/auth-gate.tsx +33 -0
- package/src/auth-email-password/web/client-plugin.ts +48 -0
- package/src/auth-email-password/web/default-topbar-actions.tsx +47 -0
- package/src/auth-email-password/web/forgot-password-screen.tsx +110 -0
- package/src/auth-email-password/web/index.ts +56 -0
- package/src/auth-email-password/web/invite-accept-screen.tsx +220 -0
- package/src/auth-email-password/web/login-screen.tsx +150 -0
- package/src/auth-email-password/web/reset-password-screen.tsx +152 -0
- package/src/auth-email-password/web/session.tsx +171 -0
- package/src/auth-email-password/web/signup-complete-screen.tsx +150 -0
- package/src/auth-email-password/web/signup-screen.tsx +130 -0
- package/src/auth-email-password/web/tenant-switcher.tsx +116 -0
- package/src/auth-email-password/web/use-shell-user.ts +34 -0
- package/src/auth-email-password/web/user-menu.tsx +89 -0
- package/src/auth-email-password/web/verify-email-screen.tsx +102 -0
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +568 -0
- package/src/billing-foundation/__tests__/feature.test.ts +110 -0
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +199 -0
- package/src/billing-foundation/aggregate-id.ts +21 -0
- package/src/billing-foundation/constants.ts +70 -0
- package/src/billing-foundation/entities.ts +50 -0
- package/src/billing-foundation/events.ts +71 -0
- package/src/billing-foundation/feature.ts +122 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +39 -0
- package/src/billing-foundation/handlers/create-checkout-session.write.ts +79 -0
- package/src/billing-foundation/handlers/create-portal-session.write.ts +73 -0
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +20 -0
- package/src/billing-foundation/handlers/process-event.write.ts +160 -0
- package/src/billing-foundation/index.ts +42 -0
- package/src/billing-foundation/projection.ts +135 -0
- package/src/billing-foundation/types.ts +157 -0
- package/src/billing-foundation/webhook-handler.ts +184 -0
- package/src/cap-counter/__tests__/cap-counter.integration.ts +566 -0
- package/src/cap-counter/__tests__/enforce-cap.test.ts +422 -0
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +265 -0
- package/src/cap-counter/aggregate-id.ts +61 -0
- package/src/cap-counter/constants.ts +32 -0
- package/src/cap-counter/enforce-cap.ts +404 -0
- package/src/cap-counter/entity.ts +48 -0
- package/src/cap-counter/feature.ts +90 -0
- package/src/cap-counter/handlers/get-counter.query.ts +43 -0
- package/src/cap-counter/handlers/increment-rolling.write.ts +79 -0
- package/src/cap-counter/handlers/increment.write.ts +92 -0
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +57 -0
- package/src/cap-counter/index.ts +34 -0
- package/src/cap-counter/with-cap-enforcement.ts +179 -0
- package/src/channel-email/email-channel.ts +48 -0
- package/src/channel-email/feature.ts +15 -0
- package/src/channel-email/index.ts +4 -0
- package/src/channel-email/smtp-transport.ts +65 -0
- package/src/channel-email/types.ts +34 -0
- package/src/channel-in-app/constants.ts +11 -0
- package/src/channel-in-app/feature.ts +30 -0
- package/src/channel-in-app/handlers/inbox.query.ts +28 -0
- package/src/channel-in-app/handlers/mark-all-read.write.ts +21 -0
- package/src/channel-in-app/handlers/mark-read.write.ts +32 -0
- package/src/channel-in-app/handlers/unread-count.query.ts +20 -0
- package/src/channel-in-app/in-app-channel.ts +44 -0
- package/src/channel-in-app/index.ts +4 -0
- package/src/channel-in-app/tables.ts +22 -0
- package/src/channel-push/feature.ts +15 -0
- package/src/channel-push/index.ts +3 -0
- package/src/channel-push/push-channel.ts +33 -0
- package/src/channel-push/types.ts +22 -0
- package/src/config/__tests__/app-overrides.test.ts +118 -0
- package/src/config/__tests__/config.integration.ts +1246 -0
- package/src/config/constants.ts +23 -0
- package/src/config/feature.ts +117 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +209 -0
- package/src/config/handlers/reset.write.ts +45 -0
- package/src/config/handlers/schema.query.ts +22 -0
- package/src/config/handlers/set.write.ts +93 -0
- package/src/config/handlers/values.query.ts +43 -0
- package/src/config/index.ts +15 -0
- package/src/config/resolver.ts +283 -0
- package/src/config/table.ts +35 -0
- package/src/config/write-helpers.ts +268 -0
- package/src/delivery/__tests__/delivery-events.integration.ts +166 -0
- package/src/delivery/__tests__/delivery.integration.ts +1405 -0
- package/src/delivery/constants.ts +33 -0
- package/src/delivery/delivery-service.ts +489 -0
- package/src/delivery/events.ts +18 -0
- package/src/delivery/feature.ts +70 -0
- package/src/delivery/handlers/log.query.ts +21 -0
- package/src/delivery/handlers/preferences.query.ts +18 -0
- package/src/delivery/handlers/set-preference.write.ts +28 -0
- package/src/delivery/index.ts +35 -0
- package/src/delivery/tables.ts +74 -0
- package/src/delivery/testing.ts +47 -0
- package/src/delivery/types.ts +71 -0
- package/src/delivery/unsubscribe.ts +99 -0
- package/src/delivery/upsert-preference.ts +145 -0
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +687 -0
- package/src/feature-toggles/constants.ts +20 -0
- package/src/feature-toggles/events.ts +18 -0
- package/src/feature-toggles/feature.ts +98 -0
- package/src/feature-toggles/global-feature-state-table.ts +28 -0
- package/src/feature-toggles/handlers/list.query.ts +26 -0
- package/src/feature-toggles/handlers/registered.query.ts +56 -0
- package/src/feature-toggles/handlers/set.write.ts +158 -0
- package/src/feature-toggles/index.ts +9 -0
- package/src/feature-toggles/toggle-runtime.ts +73 -0
- package/src/file-foundation/__tests__/feature.test.ts +35 -0
- package/src/file-foundation/__tests__/file-foundation.integration.ts +235 -0
- package/src/file-foundation/feature.ts +123 -0
- package/src/file-foundation/index.ts +7 -0
- package/src/file-provider-inmemory/__tests__/feature.test.ts +35 -0
- package/src/file-provider-inmemory/feature.ts +73 -0
- package/src/file-provider-inmemory/index.ts +3 -0
- package/src/file-provider-s3/__tests__/feature.test.ts +54 -0
- package/src/file-provider-s3/feature.ts +169 -0
- package/src/file-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/__tests__/env-helper.test.ts +161 -0
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +134 -0
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +36 -0
- package/src/files-provider-s3/env-helper.ts +49 -0
- package/src/files-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/s3-provider.ts +114 -0
- package/src/foundation-shared/config-helpers.ts +67 -0
- package/src/foundation-shared/index.ts +4 -0
- package/src/jobs/__tests__/job-system-user.integration.ts +194 -0
- package/src/jobs/__tests__/jobs-events.integration.ts +143 -0
- package/src/jobs/__tests__/jobs-feature.integration.ts +342 -0
- package/src/jobs/constants.ts +21 -0
- package/src/jobs/events.ts +39 -0
- package/src/jobs/feature.ts +150 -0
- package/src/jobs/handlers/detail.query.ts +30 -0
- package/src/jobs/handlers/list.query.ts +36 -0
- package/src/jobs/handlers/retry.write.ts +69 -0
- package/src/jobs/handlers/trigger.write.ts +39 -0
- package/src/jobs/index.ts +5 -0
- package/src/jobs/job-run-logger.ts +213 -0
- package/src/jobs/job-run-table.ts +55 -0
- package/src/legal-pages/README.md +195 -0
- package/src/legal-pages/__tests__/legal-pages.integration.ts +361 -0
- package/src/legal-pages/constants.ts +36 -0
- package/src/legal-pages/feature.ts +187 -0
- package/src/legal-pages/index.ts +13 -0
- package/src/legal-pages/markdown.ts +69 -0
- package/src/mail-foundation/__tests__/feature.test.ts +46 -0
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +247 -0
- package/src/mail-foundation/feature.ts +160 -0
- package/src/mail-foundation/index.ts +14 -0
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +37 -0
- package/src/mail-transport-inmemory/feature.ts +90 -0
- package/src/mail-transport-inmemory/index.ts +3 -0
- package/src/mail-transport-smtp/__tests__/feature.test.ts +61 -0
- package/src/mail-transport-smtp/feature.ts +182 -0
- package/src/mail-transport-smtp/index.ts +3 -0
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +84 -0
- package/src/rate-limiting/constants.ts +9 -0
- package/src/rate-limiting/feature.ts +16 -0
- package/src/rate-limiting/handlers/status.query.ts +52 -0
- package/src/rate-limiting/index.ts +2 -0
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +97 -0
- package/src/renderer-simple/feature.ts +12 -0
- package/src/renderer-simple/index.ts +2 -0
- package/src/renderer-simple/simple-renderer.ts +72 -0
- package/src/secrets/__tests__/rotate.integration.ts +176 -0
- package/src/secrets/__tests__/secrets-events.integration.ts +125 -0
- package/src/secrets/__tests__/secrets.integration.ts +118 -0
- package/src/secrets/feature.ts +84 -0
- package/src/secrets/handlers/delete.write.ts +20 -0
- package/src/secrets/handlers/list.query.ts +38 -0
- package/src/secrets/handlers/rotate.job.ts +193 -0
- package/src/secrets/handlers/set.write.ts +50 -0
- package/src/secrets/index.ts +16 -0
- package/src/secrets/secrets-context.ts +296 -0
- package/src/secrets/table.ts +68 -0
- package/src/sessions/__tests__/cleanup.integration.ts +175 -0
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +202 -0
- package/src/sessions/__tests__/sessions.integration.ts +472 -0
- package/src/sessions/__tests__/test-helpers.ts +66 -0
- package/src/sessions/constants.ts +43 -0
- package/src/sessions/feature.ts +84 -0
- package/src/sessions/handlers/cleanup.job.ts +109 -0
- package/src/sessions/handlers/list.query.ts +35 -0
- package/src/sessions/handlers/mine.query.ts +37 -0
- package/src/sessions/handlers/revoke-all-others.write.ts +42 -0
- package/src/sessions/handlers/revoke.write.ts +76 -0
- package/src/sessions/index.ts +17 -0
- package/src/sessions/schema/index.ts +5 -0
- package/src/sessions/schema/user-session.ts +67 -0
- package/src/sessions/session-callbacks.ts +110 -0
- package/src/sessions/testing.ts +42 -0
- package/src/subscription-mollie/__tests__/feature.test.ts +106 -0
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +421 -0
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +388 -0
- package/src/subscription-mollie/constants.ts +33 -0
- package/src/subscription-mollie/feature.ts +144 -0
- package/src/subscription-mollie/index.ts +13 -0
- package/src/subscription-mollie/plugin-methods.ts +79 -0
- package/src/subscription-mollie/verify-webhook.ts +244 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +98 -0
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +161 -0
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +315 -0
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +306 -0
- package/src/subscription-stripe/constants.ts +20 -0
- package/src/subscription-stripe/feature.ts +120 -0
- package/src/subscription-stripe/index.ts +14 -0
- package/src/subscription-stripe/plugin-methods.ts +91 -0
- package/src/subscription-stripe/verify-webhook.ts +235 -0
- package/src/tenant/__tests__/multi-tenant.integration.ts +278 -0
- package/src/tenant/__tests__/seed-testing.integration.ts +229 -0
- package/src/tenant/__tests__/tenant.integration.ts +347 -0
- package/src/tenant/command-schemas.ts +37 -0
- package/src/tenant/constants.ts +37 -0
- package/src/tenant/feature.ts +109 -0
- package/src/tenant/handlers/active-tenant-ids.query.ts +19 -0
- package/src/tenant/handlers/add-member.write.ts +53 -0
- package/src/tenant/handlers/cancel-invitation.write.ts +87 -0
- package/src/tenant/handlers/create.write.ts +21 -0
- package/src/tenant/handlers/disable.write.ts +18 -0
- package/src/tenant/handlers/invitations.query.ts +31 -0
- package/src/tenant/handlers/list.query.ts +17 -0
- package/src/tenant/handlers/me.query.ts +17 -0
- package/src/tenant/handlers/members.query.ts +22 -0
- package/src/tenant/handlers/memberships.query.ts +24 -0
- package/src/tenant/handlers/remove-member.write.ts +40 -0
- package/src/tenant/handlers/resolve-user-ids.query.ts +43 -0
- package/src/tenant/handlers/update-member-roles.write.ts +54 -0
- package/src/tenant/handlers/update.write.ts +20 -0
- package/src/tenant/index.ts +12 -0
- package/src/tenant/invitation-table.ts +93 -0
- package/src/tenant/membership-table.ts +35 -0
- package/src/tenant/schema/index.ts +5 -0
- package/src/tenant/schema/tenant.ts +27 -0
- package/src/tenant/seeding.ts +155 -0
- package/src/tenant/testing.ts +8 -0
- package/src/text-content/README.md +190 -0
- package/src/text-content/__tests__/text-content.integration.ts +415 -0
- package/src/text-content/api.ts +92 -0
- package/src/text-content/constants.ts +19 -0
- package/src/text-content/feature.ts +29 -0
- package/src/text-content/handlers/by-slug.query.ts +55 -0
- package/src/text-content/handlers/set.write.ts +118 -0
- package/src/text-content/index.ts +14 -0
- package/src/text-content/seeding.ts +91 -0
- package/src/text-content/table.ts +45 -0
- package/src/tier-engine/__tests__/compose-app.test.ts +182 -0
- package/src/tier-engine/__tests__/drift.test.ts +42 -0
- package/src/tier-engine/__tests__/tier-engine.integration.ts +241 -0
- package/src/tier-engine/aggregate-id.ts +27 -0
- package/src/tier-engine/compose-app.ts +150 -0
- package/src/tier-engine/constants.ts +15 -0
- package/src/tier-engine/entity.ts +30 -0
- package/src/tier-engine/feature.ts +72 -0
- package/src/tier-engine/handlers/active-tier.query.ts +23 -0
- package/src/tier-engine/index.ts +22 -0
- package/src/user/__tests__/seed-testing.integration.ts +127 -0
- package/src/user/__tests__/user.integration.ts +198 -0
- package/src/user/command-schemas.ts +15 -0
- package/src/user/constants.ts +23 -0
- package/src/user/feature.ts +32 -0
- package/src/user/handlers/create.write.ts +54 -0
- package/src/user/handlers/detail.query.ts +9 -0
- package/src/user/handlers/find-for-auth.query.ts +38 -0
- package/src/user/handlers/list.query.ts +8 -0
- package/src/user/handlers/me.query.ts +15 -0
- package/src/user/handlers/update.write.ts +54 -0
- package/src/user/index.ts +4 -0
- package/src/user/schema/index.ts +5 -0
- package/src/user/schema/user.ts +69 -0
- package/src/user/seeding.ts +93 -0
- package/src/user/testing.ts +5 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Integration test for the sessions cleanup job. Pattern mirrors
|
|
2
|
+
// secrets/retention.integration.ts — we hit the handler directly with a
|
|
3
|
+
// minimal ctx, because the full setupTestStack + jobRunner path is
|
|
4
|
+
// exercised by the framework's job tests. Here we pin the semantics: old
|
|
5
|
+
// expired/revoked rows go, live rows stay, batching + signal work.
|
|
6
|
+
|
|
7
|
+
import type { AppContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import {
|
|
9
|
+
createEntityTable,
|
|
10
|
+
setupTestStack,
|
|
11
|
+
type TestStack,
|
|
12
|
+
testTenantId,
|
|
13
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
14
|
+
import { sql } from "drizzle-orm";
|
|
15
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
16
|
+
import { createSessionsFeature } from "../feature";
|
|
17
|
+
import { cleanupJob } from "../handlers/cleanup.job";
|
|
18
|
+
import { userSessionEntity, userSessionTable } from "../schema/user-session";
|
|
19
|
+
|
|
20
|
+
type Log = NonNullable<AppContext["log"]>;
|
|
21
|
+
function silentLogger(): Log {
|
|
22
|
+
const noop = () => {};
|
|
23
|
+
const logger: Log = {
|
|
24
|
+
info: noop,
|
|
25
|
+
warn: noop,
|
|
26
|
+
error: noop,
|
|
27
|
+
debug: noop,
|
|
28
|
+
child: () => logger,
|
|
29
|
+
};
|
|
30
|
+
return logger;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const TENANT = testTenantId(1);
|
|
34
|
+
|
|
35
|
+
let stack: TestStack;
|
|
36
|
+
|
|
37
|
+
beforeAll(async () => {
|
|
38
|
+
stack = await setupTestStack({
|
|
39
|
+
features: [createSessionsFeature()],
|
|
40
|
+
});
|
|
41
|
+
await createEntityTable(stack.db, userSessionEntity);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterAll(async () => {
|
|
45
|
+
await stack.cleanup();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
beforeEach(async () => {
|
|
49
|
+
await stack.db.delete(userSessionTable);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
type JobCtx = Pick<AppContext, "db" | "registry" | "log">;
|
|
53
|
+
function jobCtx(): Parameters<typeof cleanupJob>[1] {
|
|
54
|
+
const ctx: JobCtx = {
|
|
55
|
+
db: stack.db,
|
|
56
|
+
registry: stack.registry,
|
|
57
|
+
log: silentLogger(),
|
|
58
|
+
};
|
|
59
|
+
return ctx as unknown as Parameters<typeof cleanupJob>[1];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Seed a session row at a specific age. `kind` picks which lifecycle column
|
|
63
|
+
// to back-date: "expired" sets expiresAt in the past (session lived out its
|
|
64
|
+
// window), "revoked" sets revokedAt (user logged out, time passed), "live"
|
|
65
|
+
// leaves the row current (should never be deleted).
|
|
66
|
+
async function seedSession(opts: {
|
|
67
|
+
id: string;
|
|
68
|
+
userId: string;
|
|
69
|
+
kind: "live" | "expired" | "revoked";
|
|
70
|
+
ageDays: number;
|
|
71
|
+
}): Promise<void> {
|
|
72
|
+
const now = sql`now()`;
|
|
73
|
+
const pastCreated = sql`now() - ${sql.raw(`interval '${opts.ageDays + 1} days'`)}`;
|
|
74
|
+
const past = sql`now() - ${sql.raw(`interval '${opts.ageDays} days'`)}`;
|
|
75
|
+
const future = sql`now() + ${sql.raw(`interval '30 days'`)}`;
|
|
76
|
+
|
|
77
|
+
await stack.db.insert(userSessionTable).values({
|
|
78
|
+
id: opts.id,
|
|
79
|
+
tenantId: TENANT,
|
|
80
|
+
userId: opts.userId,
|
|
81
|
+
createdAt: pastCreated,
|
|
82
|
+
expiresAt: opts.kind === "expired" ? past : future,
|
|
83
|
+
revokedAt: opts.kind === "revoked" ? past : null,
|
|
84
|
+
ip: "test",
|
|
85
|
+
userAgent: "test",
|
|
86
|
+
modifiedAt: now,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function countSessions(): Promise<number> {
|
|
91
|
+
const rows = await stack.db.select().from(userSessionTable);
|
|
92
|
+
return rows.length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("sessions cleanup job — purge expired/revoked rows", () => {
|
|
96
|
+
test("deletes expired-past-cutoff rows but keeps live ones", async () => {
|
|
97
|
+
await seedSession({
|
|
98
|
+
id: "11111111-1111-1111-1111-111111111111",
|
|
99
|
+
userId: "aa000000-0000-0000-0000-000000000001",
|
|
100
|
+
kind: "expired",
|
|
101
|
+
ageDays: 45,
|
|
102
|
+
});
|
|
103
|
+
await seedSession({
|
|
104
|
+
id: "22222222-2222-2222-2222-222222222222",
|
|
105
|
+
userId: "aa000000-0000-0000-0000-000000000002",
|
|
106
|
+
kind: "live",
|
|
107
|
+
ageDays: 1,
|
|
108
|
+
});
|
|
109
|
+
expect(await countSessions()).toBe(2);
|
|
110
|
+
|
|
111
|
+
await cleanupJob({}, jobCtx());
|
|
112
|
+
|
|
113
|
+
expect(await countSessions()).toBe(1);
|
|
114
|
+
const [remaining] = await stack.db.select().from(userSessionTable);
|
|
115
|
+
expect(remaining?.["revokedAt"]).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("deletes long-revoked rows", async () => {
|
|
119
|
+
await seedSession({
|
|
120
|
+
id: "33333333-3333-3333-3333-333333333333",
|
|
121
|
+
userId: "bb000000-0000-0000-0000-000000000001",
|
|
122
|
+
kind: "revoked",
|
|
123
|
+
ageDays: 60,
|
|
124
|
+
});
|
|
125
|
+
expect(await countSessions()).toBe(1);
|
|
126
|
+
|
|
127
|
+
await cleanupJob({}, jobCtx());
|
|
128
|
+
|
|
129
|
+
expect(await countSessions()).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("recently-revoked rows stay around (inside retention window)", async () => {
|
|
133
|
+
await seedSession({
|
|
134
|
+
id: "44444444-4444-4444-4444-444444444444",
|
|
135
|
+
userId: "cc000000-0000-0000-0000-000000000001",
|
|
136
|
+
kind: "revoked",
|
|
137
|
+
ageDays: 10,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Default 30d window: 10d-old revoked row stays
|
|
141
|
+
await cleanupJob({}, jobCtx());
|
|
142
|
+
|
|
143
|
+
expect(await countSessions()).toBe(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("olderThanDays override respects custom windows", async () => {
|
|
147
|
+
await seedSession({
|
|
148
|
+
id: "55555555-5555-5555-5555-555555555555",
|
|
149
|
+
userId: "dd000000-0000-0000-0000-000000000001",
|
|
150
|
+
kind: "revoked",
|
|
151
|
+
ageDays: 5,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Tight 3d window: the 5d row goes
|
|
155
|
+
await cleanupJob({ olderThanDays: 3 }, jobCtx());
|
|
156
|
+
|
|
157
|
+
expect(await countSessions()).toBe(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("batching drains a large backlog across chunks", async () => {
|
|
161
|
+
for (let i = 0; i < 7; i++) {
|
|
162
|
+
await seedSession({
|
|
163
|
+
id: `99999999-9999-9999-9999-${String(i).padStart(12, "0")}`,
|
|
164
|
+
userId: "ee000000-0000-0000-0000-000000000001",
|
|
165
|
+
kind: "expired",
|
|
166
|
+
ageDays: 60,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
expect(await countSessions()).toBe(7);
|
|
170
|
+
|
|
171
|
+
await cleanupJob({ olderThanDays: 30, batchSize: 2 }, jobCtx());
|
|
172
|
+
|
|
173
|
+
expect(await countSessions()).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import {
|
|
5
|
+
createEntityTable,
|
|
6
|
+
pushTables,
|
|
7
|
+
setupTestStack,
|
|
8
|
+
type TestStack,
|
|
9
|
+
testTenantId,
|
|
10
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
11
|
+
import { createLateBoundHolder } from "@cosmicdrift/kumiko-framework/testing";
|
|
12
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
|
13
|
+
import { AuthHandlers } from "../../auth-email-password/constants";
|
|
14
|
+
import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
|
|
15
|
+
import { createConfigFeature } from "../../config";
|
|
16
|
+
import { createConfigResolver } from "../../config/resolver";
|
|
17
|
+
import { configValuesTable } from "../../config/table";
|
|
18
|
+
import { createTenantFeature } from "../../tenant";
|
|
19
|
+
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
20
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
21
|
+
import { UserHandlers } from "../../user";
|
|
22
|
+
import { createUserFeature } from "../../user/feature";
|
|
23
|
+
import { userEntity, userTable } from "../../user/schema/user";
|
|
24
|
+
import { createSessionsFeature } from "../feature";
|
|
25
|
+
import { userSessionEntity, userSessionTable } from "../schema/user-session";
|
|
26
|
+
import { createSessionCallbacks, type SessionCallbacks } from "../session-callbacks";
|
|
27
|
+
import { sessionCallbacksFromLateBound } from "../testing";
|
|
28
|
+
import { makeSessionHelpers } from "./test-helpers";
|
|
29
|
+
|
|
30
|
+
// When a user changes their password, every live session for that user must
|
|
31
|
+
// stop working — the industry-standard "signs you out everywhere" rule.
|
|
32
|
+
// Proves the sessions-feature wires the user-entity postSave hook correctly
|
|
33
|
+
// and the mass-revoker does the full sweep (including the caller's session).
|
|
34
|
+
|
|
35
|
+
let stack: TestStack;
|
|
36
|
+
let h: ReturnType<typeof makeSessionHelpers>;
|
|
37
|
+
const callbacks = createLateBoundHolder<SessionCallbacks>("session-callbacks");
|
|
38
|
+
|
|
39
|
+
// vi.fn spy for the revoker — lets us assert exact call counts and arguments
|
|
40
|
+
// per test without leaking module-level mutable state across suites.
|
|
41
|
+
const massRevokeSpy = vi.fn<(userId: string) => Promise<number>>();
|
|
42
|
+
|
|
43
|
+
const encryptionKey = randomBytes(32).toString("base64");
|
|
44
|
+
|
|
45
|
+
// Align with TestUsers.systemAdmin.tenantId so seed + change-password write
|
|
46
|
+
// events onto the same stream. Mismatched tenants land create on A and
|
|
47
|
+
// update on B — getStreamVersion returns 0 and optimistic-lock fails.
|
|
48
|
+
const TENANT: TenantId = testTenantId(1);
|
|
49
|
+
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
const encryption = createEncryptionProvider(encryptionKey);
|
|
52
|
+
const resolver = createConfigResolver({ encryption });
|
|
53
|
+
const bound = sessionCallbacksFromLateBound(callbacks);
|
|
54
|
+
const baseRevoker = bound.asMassRevoker();
|
|
55
|
+
|
|
56
|
+
// Wire the spy as the revoker passed to the feature; it forwards to the
|
|
57
|
+
// real one so the DB stays in sync, but also records the call.
|
|
58
|
+
massRevokeSpy.mockImplementation((userId) => baseRevoker(userId));
|
|
59
|
+
|
|
60
|
+
stack = await setupTestStack({
|
|
61
|
+
features: [
|
|
62
|
+
createConfigFeature(),
|
|
63
|
+
createUserFeature(),
|
|
64
|
+
createTenantFeature(),
|
|
65
|
+
createAuthEmailPasswordFeature(),
|
|
66
|
+
createSessionsFeature({
|
|
67
|
+
autoRevokeOnPasswordChange: massRevokeSpy,
|
|
68
|
+
}),
|
|
69
|
+
],
|
|
70
|
+
extraContext: { configResolver: resolver, configEncryption: encryption },
|
|
71
|
+
authConfig: {
|
|
72
|
+
...bound.asAuthConfig(),
|
|
73
|
+
membershipQuery: "tenant:query:memberships",
|
|
74
|
+
loginHandler: AuthHandlers.login,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
callbacks.set(createSessionCallbacks({ db: stack.db }));
|
|
78
|
+
h = makeSessionHelpers(stack, TENANT);
|
|
79
|
+
|
|
80
|
+
await createEntityTable(stack.db, userEntity);
|
|
81
|
+
await createEntityTable(stack.db, tenantEntity);
|
|
82
|
+
await createEntityTable(stack.db, userSessionEntity);
|
|
83
|
+
await pushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterAll(async () => {
|
|
87
|
+
await stack.cleanup();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
beforeEach(async () => {
|
|
91
|
+
await stack.db.delete(userTable);
|
|
92
|
+
await stack.db.delete(tenantMembershipsTable);
|
|
93
|
+
await stack.db.delete(userSessionTable);
|
|
94
|
+
massRevokeSpy.mockClear();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("password change mass-revokes every live session", () => {
|
|
98
|
+
test("changing the password revokes ALL sessions including the caller's", async () => {
|
|
99
|
+
await h.seedUser("rotate@example.com", "first-password");
|
|
100
|
+
|
|
101
|
+
const a = await h.login("rotate@example.com", "first-password");
|
|
102
|
+
const b = await h.login("rotate@example.com", "first-password");
|
|
103
|
+
const c = await h.login("rotate@example.com", "first-password");
|
|
104
|
+
|
|
105
|
+
// Sanity: all three are currently live and queries go through
|
|
106
|
+
expect(
|
|
107
|
+
(await h.authedPost("/api/query", a.token, { type: "user:query:user:me", payload: {} }))
|
|
108
|
+
.status,
|
|
109
|
+
).toBe(200);
|
|
110
|
+
expect(
|
|
111
|
+
(await h.authedPost("/api/query", b.token, { type: "user:query:user:me", payload: {} }))
|
|
112
|
+
.status,
|
|
113
|
+
).toBe(200);
|
|
114
|
+
expect(
|
|
115
|
+
(await h.authedPost("/api/query", c.token, { type: "user:query:user:me", payload: {} }))
|
|
116
|
+
.status,
|
|
117
|
+
).toBe(200);
|
|
118
|
+
|
|
119
|
+
// b changes the password via its own JWT
|
|
120
|
+
const change = await h.authedPost("/api/write", b.token, {
|
|
121
|
+
type: AuthHandlers.changePassword,
|
|
122
|
+
payload: { oldPassword: "first-password", newPassword: "second-password-long" },
|
|
123
|
+
});
|
|
124
|
+
expect(change.status).toBe(200);
|
|
125
|
+
|
|
126
|
+
// Revoker was called exactly once; the return-value reports the 3 live
|
|
127
|
+
// sessions it revoked (a + b + c, all for the same user).
|
|
128
|
+
expect(massRevokeSpy).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(await massRevokeSpy.mock.results[0]?.value).toBe(3);
|
|
130
|
+
|
|
131
|
+
// Every previously-live session — INCLUDING b — is now revoked
|
|
132
|
+
expect(
|
|
133
|
+
(await h.authedPost("/api/query", a.token, { type: "user:query:user:me", payload: {} }))
|
|
134
|
+
.status,
|
|
135
|
+
).toBe(401);
|
|
136
|
+
expect(
|
|
137
|
+
(await h.authedPost("/api/query", b.token, { type: "user:query:user:me", payload: {} }))
|
|
138
|
+
.status,
|
|
139
|
+
).toBe(401);
|
|
140
|
+
expect(
|
|
141
|
+
(await h.authedPost("/api/query", c.token, { type: "user:query:user:me", payload: {} }))
|
|
142
|
+
.status,
|
|
143
|
+
).toBe(401);
|
|
144
|
+
|
|
145
|
+
// DB state confirms: zero live rows for this user
|
|
146
|
+
const liveRows = await stack.db.select().from(userSessionTable);
|
|
147
|
+
const stillLive = liveRows.filter((r) => r["revokedAt"] === null);
|
|
148
|
+
expect(stillLive).toHaveLength(0);
|
|
149
|
+
|
|
150
|
+
// And logging in again with the NEW password works
|
|
151
|
+
const loginAfter = await stack.http.raw("POST", "/api/auth/login", {
|
|
152
|
+
email: "rotate@example.com",
|
|
153
|
+
password: "second-password-long",
|
|
154
|
+
});
|
|
155
|
+
expect(loginAfter.status).toBe(200);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("user:create does NOT trigger mass-revoke (isNew guard)", async () => {
|
|
159
|
+
// seedUser does a user:create — the hook fires, but the isNew guard
|
|
160
|
+
// should short-circuit before the mass-revoker runs. A future refactor
|
|
161
|
+
// that drops the guard would make the spy show a call here.
|
|
162
|
+
await h.seedUser("fresh@example.com", "pw-long-enough");
|
|
163
|
+
expect(massRevokeSpy).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("editing a non-password field does NOT trigger mass-revoke", async () => {
|
|
167
|
+
const { userId } = await h.seedUser("stable@example.com", "pw-long-enough");
|
|
168
|
+
const a = await h.login("stable@example.com", "pw-long-enough");
|
|
169
|
+
|
|
170
|
+
// Grab the version number so the user:update handler passes the
|
|
171
|
+
// optimistic-lock check. `me` returns the current row to the caller
|
|
172
|
+
// with version included.
|
|
173
|
+
const meRes = await h.authedPost("/api/query", a.token, {
|
|
174
|
+
type: "user:query:user:me",
|
|
175
|
+
payload: {},
|
|
176
|
+
});
|
|
177
|
+
expect(meRes.status).toBe(200);
|
|
178
|
+
const me = (await meRes.json()) as { data: { version: number } };
|
|
179
|
+
|
|
180
|
+
// The caller updates their own displayName — must NOT sign them out.
|
|
181
|
+
const update = await h.authedPost("/api/write", a.token, {
|
|
182
|
+
type: UserHandlers.update,
|
|
183
|
+
payload: {
|
|
184
|
+
id: userId,
|
|
185
|
+
version: me.data.version,
|
|
186
|
+
changes: { displayName: "New Name" },
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
expect(update.status).toBe(200);
|
|
190
|
+
|
|
191
|
+
// Revoker not called — the isNew guard + passwordHash guard both must
|
|
192
|
+
// have held off.
|
|
193
|
+
expect(massRevokeSpy).not.toHaveBeenCalled();
|
|
194
|
+
|
|
195
|
+
// Same JWT still works
|
|
196
|
+
const after = await h.authedPost("/api/query", a.token, {
|
|
197
|
+
type: "user:query:user:me",
|
|
198
|
+
payload: {},
|
|
199
|
+
});
|
|
200
|
+
expect(after.status).toBe(200);
|
|
201
|
+
});
|
|
202
|
+
});
|