@cosmicdrift/kumiko-bundled-features 0.14.0 → 0.16.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 +2 -2
- package/src/__tests__/env-schemas.test.ts +1 -1
- package/src/__tests__/es-ops-e2e.integration.ts +10 -9
- package/src/audit/__tests__/audit.integration.ts +3 -3
- package/src/audit/handlers/list.query.ts +39 -51
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +4 -3
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +4 -3
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +5 -4
- package/src/auth-email-password/__tests__/auth.integration.ts +4 -3
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-templates.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-verification.integration.ts +7 -10
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +1 -1
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +4 -3
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +16 -43
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +6 -9
- package/src/auth-email-password/__tests__/password-reset.integration.ts +8 -7
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +4 -3
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +19 -32
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +6 -5
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +1 -1
- package/src/auth-email-password/__tests__/signed-token.test.ts +1 -1
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +11 -15
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +26 -26
- package/src/auth-email-password/handlers/invite-accept.write.ts +24 -21
- package/src/auth-email-password/handlers/invite-create.write.ts +3 -8
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +20 -17
- package/src/auth-email-password/handlers/signup-confirm.write.ts +3 -7
- package/src/auth-email-password/seeding.ts +1 -1
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +1 -2
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +10 -19
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +12 -18
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +12 -17
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +1 -1
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +1 -8
- package/src/auth-email-password/web/__tests__/test-utils.tsx +4 -8
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +2 -8
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +10 -15
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +1 -1
- package/src/billing-foundation/__tests__/feature.test.ts +1 -1
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +6 -5
- package/src/billing-foundation/db/queries/subscription-projection.ts +15 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +2 -6
- package/src/billing-foundation/handlers/create-portal-session.write.ts +2 -2
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +4 -1
- package/src/billing-foundation/projection.ts +32 -13
- package/src/cap-counter/__tests__/cap-counter.integration.ts +1 -1
- package/src/cap-counter/__tests__/enforce-cap.test.ts +37 -32
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +1 -1
- package/src/cap-counter/enforce-cap.ts +14 -20
- package/src/cap-counter/handlers/get-counter.query.ts +7 -13
- package/src/cap-counter/handlers/increment.write.ts +2 -2
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
- package/src/channel-in-app/handlers/inbox.query.ts +7 -13
- package/src/channel-in-app/handlers/mark-all-read.write.ts +7 -9
- package/src/channel-in-app/handlers/mark-read.write.ts +8 -14
- package/src/channel-in-app/handlers/unread-count.query.ts +10 -9
- package/src/channel-in-app/in-app-channel.ts +10 -12
- package/src/channel-in-app/tables.ts +1 -1
- package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +1 -1
- package/src/compliance-profiles/__tests__/seeding.integration.ts +1 -1
- package/src/compliance-profiles/_internal/parse-override.ts +19 -0
- package/src/compliance-profiles/handlers/for-tenant.query.ts +10 -32
- package/src/compliance-profiles/handlers/needs-profile.query.ts +4 -7
- package/src/compliance-profiles/handlers/set-profile.write.ts +5 -7
- package/src/compliance-profiles/resolve-for-tenant.ts +11 -27
- package/src/compliance-profiles/schema/profile-selection.ts +2 -2
- package/src/compliance-profiles/seeding.ts +4 -7
- package/src/config/__tests__/app-overrides.test.ts +1 -1
- package/src/config/__tests__/cascade.integration.ts +1 -1
- package/src/config/__tests__/config.integration.ts +8 -27
- package/src/config/db/queries/resolver.ts +47 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +1 -1
- package/src/config/resolver.ts +14 -62
- package/src/config/table.ts +4 -4
- package/src/config/write-helpers.ts +7 -11
- package/src/custom-fields/__tests__/audit-integration.integration.ts +6 -6
- package/src/custom-fields/__tests__/custom-fields.integration.ts +7 -7
- package/src/custom-fields/__tests__/feature.test.ts +1 -1
- package/src/custom-fields/__tests__/field-access.integration.ts +6 -6
- package/src/custom-fields/__tests__/quota.integration.ts +6 -6
- package/src/custom-fields/__tests__/retention.integration.ts +12 -10
- package/src/custom-fields/__tests__/user-data-rights.integration.ts +27 -17
- package/src/custom-fields/__tests__/wire-for-entity.test.ts +5 -5
- package/src/custom-fields/db/queries/field-access.ts +16 -0
- package/src/custom-fields/db/queries/projection.ts +43 -0
- package/src/custom-fields/db/queries/quota.ts +14 -0
- package/src/custom-fields/db/queries/retention.ts +39 -0
- package/src/custom-fields/db/queries/user-data-rights.ts +54 -0
- package/src/custom-fields/lib/field-access.ts +2 -41
- package/src/custom-fields/lib/quota.ts +2 -25
- package/src/custom-fields/run-retention.ts +19 -21
- package/src/custom-fields/wire-for-entity.ts +30 -23
- package/src/custom-fields/wire-user-data-rights.ts +33 -85
- package/src/data-retention/__tests__/data-retention.integration.ts +1 -1
- package/src/data-retention/__tests__/keep-for.test.ts +1 -1
- package/src/data-retention/__tests__/override-schema.test.ts +1 -1
- package/src/data-retention/__tests__/policy-for.integration.ts +1 -1
- package/src/data-retention/__tests__/resolver.test.ts +1 -1
- package/src/data-retention/handlers/policy-for.query.ts +5 -8
- package/src/data-retention/resolve-for-tenant.ts +6 -8
- package/src/data-retention/schema/tenant-retention-override.ts +2 -2
- package/src/delivery/__tests__/delivery-events.integration.ts +8 -21
- package/src/delivery/__tests__/delivery.integration.ts +100 -190
- package/src/delivery/db/queries/preferences.ts +30 -0
- package/src/delivery/delivery-service.ts +8 -36
- package/src/delivery/feature.ts +10 -2
- package/src/delivery/handlers/log.query.ts +5 -7
- package/src/delivery/handlers/preferences.query.ts +2 -5
- package/src/delivery/tables.ts +26 -1
- package/src/delivery/upsert-preference.ts +8 -14
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +30 -30
- package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +7 -6
- package/src/feature-toggles/db/queries/toggle-state.ts +25 -0
- package/src/feature-toggles/feature.ts +16 -2
- package/src/feature-toggles/global-feature-state-table.ts +1 -1
- package/src/feature-toggles/handlers/list.query.ts +9 -2
- package/src/feature-toggles/handlers/registered.query.ts +3 -7
- package/src/feature-toggles/handlers/set.write.ts +37 -25
- package/src/feature-toggles/toggle-runtime.ts +3 -6
- package/src/file-foundation/__tests__/feature.test.ts +1 -1
- package/src/file-foundation/__tests__/file-foundation.integration.ts +1 -1
- package/src/file-provider-inmemory/__tests__/feature.test.ts +1 -1
- package/src/file-provider-s3/__tests__/feature.test.ts +1 -1
- package/src/files/__tests__/files.integration.ts +18 -7
- package/src/files/schema/file-ref.ts +1 -1
- package/src/files-provider-s3/__tests__/env-helper.test.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +1 -1
- package/src/jobs/__tests__/job-system-user.integration.ts +1 -1
- package/src/jobs/__tests__/jobs-events.integration.ts +8 -21
- package/src/jobs/__tests__/jobs-feature.integration.ts +1 -1
- package/src/jobs/feature.ts +26 -15
- package/src/jobs/handlers/detail.query.ts +10 -8
- package/src/jobs/handlers/list.query.ts +9 -21
- package/src/jobs/handlers/retry.write.ts +2 -7
- package/src/jobs/job-run-logger.ts +3 -9
- package/src/jobs/job-run-table.ts +49 -17
- package/src/legal-pages/__tests__/legal-pages.integration.ts +1 -1
- package/src/mail-foundation/__tests__/feature.test.ts +1 -1
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +1 -1
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +1 -1
- package/src/mail-transport-smtp/__tests__/feature.test.ts +1 -1
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +1 -1
- package/src/renderer-foundation/__tests__/api.test.ts +2 -2
- package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +1 -1
- package/src/renderer-simple/__tests__/adapter.test.ts +2 -2
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +1 -1
- package/src/secrets/__tests__/require-secrets-context.test.ts +6 -5
- package/src/secrets/__tests__/rotate.integration.ts +6 -9
- package/src/secrets/__tests__/secrets-events.integration.ts +6 -12
- package/src/secrets/__tests__/secrets.integration.ts +6 -11
- package/src/secrets/db/queries/read.ts +16 -0
- package/src/secrets/handlers/list.query.ts +16 -17
- package/src/secrets/handlers/rotate.job.ts +8 -12
- package/src/secrets/secrets-context.ts +9 -21
- package/src/secrets/table.ts +1 -1
- package/src/sessions/__tests__/cleanup.integration.ts +8 -6
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +7 -6
- package/src/sessions/__tests__/sessions.integration.ts +23 -38
- package/src/sessions/__tests__/test-helpers.ts +1 -1
- package/src/sessions/db/queries/cleanup.ts +21 -0
- package/src/sessions/handlers/cleanup.job.ts +6 -29
- package/src/sessions/handlers/list.query.ts +24 -24
- package/src/sessions/handlers/mine.query.ts +24 -23
- package/src/sessions/handlers/revoke-all-for-user.write.ts +7 -11
- package/src/sessions/handlers/revoke-all-others.write.ts +7 -12
- package/src/sessions/handlers/revoke.write.ts +11 -18
- package/src/sessions/schema/user-session.ts +2 -2
- package/src/sessions/session-callbacks.ts +19 -21
- package/src/subscription-mollie/__tests__/feature.test.ts +1 -1
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +1 -1
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +8 -7
- package/src/subscription-stripe/__tests__/feature.test.ts +1 -1
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +14 -15
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +1 -1
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +14 -14
- package/src/subscription-stripe/verify-webhook.ts +1 -1
- package/src/template-resolver/__tests__/handlers.integration.ts +1 -1
- package/src/template-resolver/__tests__/template-resolver.integration.ts +3 -2
- package/src/template-resolver/api.ts +7 -13
- package/src/template-resolver/handlers/archive.write.ts +4 -7
- package/src/template-resolver/handlers/find-by-id.query.ts +4 -7
- package/src/template-resolver/handlers/list.query.ts +13 -21
- package/src/template-resolver/handlers/publish.write.ts +4 -7
- package/src/template-resolver/handlers/upsert-system.write.ts +7 -10
- package/src/template-resolver/handlers/upsert-tenant.write.ts +7 -10
- package/src/template-resolver/table.ts +2 -5
- package/src/tenant/__tests__/multi-tenant.integration.ts +1 -1
- package/src/tenant/__tests__/seed-testing.integration.ts +19 -45
- package/src/tenant/__tests__/tenant.integration.ts +1 -1
- package/src/tenant/handlers/active-tenant-ids.query.ts +3 -8
- package/src/tenant/handlers/add-member.write.ts +6 -8
- package/src/tenant/handlers/cancel-invitation.write.ts +5 -7
- package/src/tenant/handlers/invitations.query.ts +5 -10
- package/src/tenant/handlers/me.query.ts +2 -3
- package/src/tenant/handlers/members.query.ts +4 -5
- package/src/tenant/handlers/memberships.query.ts +2 -5
- package/src/tenant/handlers/remove-member.write.ts +6 -8
- package/src/tenant/handlers/resolve-user-ids.query.ts +6 -16
- package/src/tenant/handlers/update-member-roles.write.ts +6 -8
- package/src/tenant/invitation-table.ts +2 -5
- package/src/tenant/membership-table.ts +3 -6
- package/src/tenant/schema/tenant.ts +2 -2
- package/src/tenant/seeding.ts +12 -18
- package/src/text-content/README.md +1 -1
- package/src/text-content/__tests__/text-content.integration.ts +2 -2
- package/src/text-content/api.ts +2 -9
- package/src/text-content/handlers/by-slug.query.ts +6 -9
- package/src/text-content/handlers/by-tenant.query.ts +2 -2
- package/src/text-content/handlers/set.write.ts +7 -9
- package/src/text-content/seeding.ts +6 -9
- package/src/text-content/table.ts +2 -2
- package/src/text-content/web/__tests__/editor-read-only.test.tsx +31 -45
- package/src/text-content/web/__tests__/group-blocks.test.ts +1 -18
- package/src/text-content/web/client-plugin.tsx +11 -23
- package/src/tier-engine/__tests__/auto-default-tier.integration.ts +10 -16
- package/src/tier-engine/__tests__/compose-app.test.ts +1 -1
- package/src/tier-engine/__tests__/drift.test.ts +1 -1
- package/src/tier-engine/__tests__/resolver.integration.ts +6 -6
- package/src/tier-engine/__tests__/tier-engine.integration.ts +1 -1
- package/src/tier-engine/feature.ts +9 -16
- package/src/user/__tests__/seed-testing.integration.ts +10 -22
- package/src/user/__tests__/user-status.test.ts +1 -1
- package/src/user/__tests__/user.integration.ts +6 -5
- package/src/user/handlers/create.write.ts +5 -7
- package/src/user/handlers/find-for-auth.query.ts +5 -7
- package/src/user/schema/user.ts +2 -2
- package/src/user/seeding.ts +2 -3
- package/src/user-data-rights/__tests__/audit-log.integration.ts +24 -12
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +64 -37
- package/src/user-data-rights/__tests__/download.integration.ts +29 -46
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +35 -28
- package/src/user-data-rights/__tests__/export-job-schema.test.ts +2 -2
- package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +1 -1
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +11 -15
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +10 -12
- package/src/user-data-rights/__tests__/request-export.integration.ts +23 -16
- package/src/user-data-rights/__tests__/restriction-flow.integration.ts +24 -32
- package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +142 -137
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +46 -28
- package/src/user-data-rights/__tests__/run-user-export.integration.ts +20 -14
- package/src/user-data-rights/__tests__/token-helpers.test.ts +1 -1
- package/src/user-data-rights/__tests__/user-data-rights.integration.ts +1 -1
- package/src/user-data-rights/__tests__/zip-path.test.ts +1 -1
- package/src/user-data-rights/audit-download.ts +3 -3
- package/src/user-data-rights/db/queries/export-jobs.ts +23 -0
- package/src/user-data-rights/db/queries/forget-cleanup.ts +13 -0
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +28 -22
- package/src/user-data-rights/handlers/download-by-job.query.ts +11 -21
- package/src/user-data-rights/handlers/download-by-token.query.ts +20 -35
- package/src/user-data-rights/handlers/export-status.query.ts +19 -33
- package/src/user-data-rights/handlers/lift-restriction.write.ts +7 -12
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +14 -23
- package/src/user-data-rights/handlers/my-audit-log.query.ts +33 -23
- package/src/user-data-rights/handlers/request-deletion.write.ts +15 -15
- package/src/user-data-rights/handlers/request-export.write.ts +7 -11
- package/src/user-data-rights/handlers/restrict-account.write.ts +12 -12
- package/src/user-data-rights/run-export-jobs.ts +20 -60
- package/src/user-data-rights/run-forget-cleanup.ts +19 -33
- package/src/user-data-rights/run-user-export.ts +4 -6
- package/src/user-data-rights/schema/download-attempt.ts +2 -2
- package/src/user-data-rights/schema/download-token.ts +2 -2
- package/src/user-data-rights/schema/export-job.ts +2 -3
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +37 -30
- package/src/user-data-rights-defaults/db/queries/user-hook.ts +17 -0
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +12 -27
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +16 -18
- package/CHANGELOG.md +0 -689
|
@@ -1,35 +1,35 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { desc } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { userSessionTable } from "../schema/user-session";
|
|
5
5
|
|
|
6
|
-
// Admin view of every session in the active tenant.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// whole tenant. Tenant-scoping comes from ctx.db (TenantDb applies a tenant
|
|
10
|
-
// filter automatically on select from tables with a tenantId column), so
|
|
11
|
-
// cross-tenant bleed is impossible.
|
|
12
|
-
//
|
|
13
|
-
// Includes revoked rows too — distinct column in the response tells the UI
|
|
14
|
-
// which entries are historical vs. live. The default ordering puts the
|
|
15
|
-
// newest first so a security review starts at the recent activity.
|
|
6
|
+
// Admin view of every session in the active tenant. ctx.db (TenantDb)
|
|
7
|
+
// applies tenant-scoping automatically on selects from tables with a
|
|
8
|
+
// tenantId column. Includes revoked rows; UI shows revokedAt distinct.
|
|
16
9
|
export const listQuery = defineQueryHandler({
|
|
17
10
|
name: "user-session:list",
|
|
18
11
|
schema: z.object({}),
|
|
19
12
|
access: { roles: access.admin },
|
|
20
13
|
handler: async (_query, ctx) => {
|
|
21
|
-
const rows = await
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
14
|
+
const rows = await selectMany<{
|
|
15
|
+
id: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
createdAt: unknown;
|
|
18
|
+
expiresAt: unknown;
|
|
19
|
+
revokedAt: unknown;
|
|
20
|
+
ip: string | null;
|
|
21
|
+
userAgent: string | null;
|
|
22
|
+
}>(ctx.db, userSessionTable, undefined, {
|
|
23
|
+
orderBy: { col: "createdAt", direction: "desc" },
|
|
24
|
+
});
|
|
25
|
+
return rows.map((r) => ({
|
|
26
|
+
id: r.id,
|
|
27
|
+
userId: r.userId,
|
|
28
|
+
createdAt: r.createdAt,
|
|
29
|
+
expiresAt: r.expiresAt,
|
|
30
|
+
revokedAt: r.revokedAt,
|
|
31
|
+
ip: r.ip,
|
|
32
|
+
userAgent: r.userAgent,
|
|
33
|
+
}));
|
|
34
34
|
},
|
|
35
35
|
});
|
|
@@ -1,37 +1,38 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { and, desc, eq, isNull } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { userSessionTable } from "../schema/user-session";
|
|
5
5
|
|
|
6
6
|
// "My live sessions" — the backing data for a devices/sessions UI. Returns
|
|
7
7
|
// ONLY the current user's own, currently-live sessions, ordered by most-
|
|
8
|
-
// recently-used first. Revoked rows
|
|
9
|
-
// audit but the UI shouldn't show them as active).
|
|
10
|
-
//
|
|
11
|
-
// Note the `current` marker: we compare against the caller's `user.sid` so
|
|
12
|
-
// the UI can label the entry the user is looking at ("this device"). A user
|
|
13
|
-
// without a sid (stateless-JWT deployment) will simply see `current: false`
|
|
14
|
-
// on every row — the feature still works, just without the marker.
|
|
8
|
+
// recently-used first. Revoked rows excluded (revokedAt IS NULL).
|
|
15
9
|
export const mineQuery = defineQueryHandler({
|
|
16
10
|
name: "user-session:mine",
|
|
17
11
|
schema: z.object({}),
|
|
18
12
|
access: { openToAll: true },
|
|
19
13
|
handler: async (query, ctx) => {
|
|
20
|
-
const rows = await
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
14
|
+
const rows = await selectMany<{
|
|
15
|
+
id: string;
|
|
16
|
+
createdAt: unknown;
|
|
17
|
+
expiresAt: unknown;
|
|
18
|
+
ip: string | null;
|
|
19
|
+
userAgent: string | null;
|
|
20
|
+
}>(
|
|
21
|
+
ctx.db,
|
|
22
|
+
userSessionTable,
|
|
23
|
+
{ userId: query.user.id, revokedAt: null },
|
|
24
|
+
{
|
|
25
|
+
orderBy: { col: "createdAt", direction: "desc" },
|
|
26
|
+
},
|
|
27
|
+
);
|
|
34
28
|
const currentSid = query.user.sid;
|
|
35
|
-
return rows.map((r) => ({
|
|
29
|
+
return rows.map((r) => ({
|
|
30
|
+
id: r.id,
|
|
31
|
+
createdAt: r.createdAt,
|
|
32
|
+
expiresAt: r.expiresAt,
|
|
33
|
+
ip: r.ip,
|
|
34
|
+
userAgent: r.userAgent,
|
|
35
|
+
current: currentSid === r.id,
|
|
36
|
+
}));
|
|
36
37
|
},
|
|
37
38
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { and, eq, isNull } from "drizzle-orm";
|
|
3
3
|
import { Temporal } from "temporal-polyfill";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { userSessionTable } from "../schema/user-session";
|
|
@@ -23,16 +23,12 @@ export const revokeAllForUserWrite = defineWriteHandler({
|
|
|
23
23
|
}),
|
|
24
24
|
access: { roles: access.privileged },
|
|
25
25
|
handler: async (event, ctx) => {
|
|
26
|
-
const updated = await
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
isNull(userSessionTable["revokedAt"]),
|
|
33
|
-
),
|
|
34
|
-
)
|
|
35
|
-
.returning();
|
|
26
|
+
const updated = await updateMany(
|
|
27
|
+
ctx.db.raw,
|
|
28
|
+
userSessionTable,
|
|
29
|
+
{ revokedAt: Temporal.Now.instant() },
|
|
30
|
+
{ userId: event.payload.userId, revokedAt: null },
|
|
31
|
+
);
|
|
36
32
|
|
|
37
33
|
return {
|
|
38
34
|
isSuccess: true as const,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
-
import { and, eq, isNull, ne } from "drizzle-orm";
|
|
4
4
|
import { Temporal } from "temporal-polyfill";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { SessionErrors } from "../constants";
|
|
@@ -25,17 +25,12 @@ export const revokeAllOthersWrite = defineWriteHandler({
|
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const updated = await
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
isNull(userSessionTable["revokedAt"]),
|
|
35
|
-
ne(userSessionTable["id"], keepSid),
|
|
36
|
-
),
|
|
37
|
-
)
|
|
38
|
-
.returning();
|
|
28
|
+
const updated = await updateMany(
|
|
29
|
+
ctx.db,
|
|
30
|
+
userSessionTable,
|
|
31
|
+
{ revokedAt: Temporal.Now.instant() },
|
|
32
|
+
{ userId: event.user.id, revokedAt: null, id: { ne: keepSid } },
|
|
33
|
+
);
|
|
39
34
|
|
|
40
35
|
return { isSuccess: true, data: { count: updated.length } };
|
|
41
36
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { fetchOne, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
-
import { and, eq, isNull } from "drizzle-orm";
|
|
4
4
|
import { Temporal } from "temporal-polyfill";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { SessionErrors } from "../constants";
|
|
@@ -28,17 +28,12 @@ export const revokeWrite = defineWriteHandler({
|
|
|
28
28
|
}),
|
|
29
29
|
access: { openToAll: true },
|
|
30
30
|
handler: async (event, ctx) => {
|
|
31
|
-
const updated = await
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
eq(userSessionTable["userId"], event.user.id),
|
|
38
|
-
isNull(userSessionTable["revokedAt"]),
|
|
39
|
-
),
|
|
40
|
-
)
|
|
41
|
-
.returning();
|
|
31
|
+
const updated = await updateMany(
|
|
32
|
+
ctx.db,
|
|
33
|
+
userSessionTable,
|
|
34
|
+
{ revokedAt: Temporal.Now.instant() },
|
|
35
|
+
{ id: event.payload.id, userId: event.user.id, revokedAt: null },
|
|
36
|
+
);
|
|
42
37
|
|
|
43
38
|
if (updated.length > 0) {
|
|
44
39
|
return { isSuccess: true, data: { id: event.payload.id } };
|
|
@@ -46,13 +41,11 @@ export const revokeWrite = defineWriteHandler({
|
|
|
46
41
|
|
|
47
42
|
// Zero rows touched — disambiguate between "not yours" and "already
|
|
48
43
|
// revoked" via a point-read. Only hits on the error path.
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.where(eq(userSessionTable["id"], event.payload.id))
|
|
53
|
-
.limit(1);
|
|
44
|
+
const row = await fetchOne<{ userId: string; revokedAt: unknown }>(ctx.db, userSessionTable, {
|
|
45
|
+
id: event.payload.id,
|
|
46
|
+
});
|
|
54
47
|
|
|
55
|
-
if (row && row
|
|
48
|
+
if (row && row.userId === event.user.id && row.revokedAt !== null) {
|
|
56
49
|
return writeFailure(
|
|
57
50
|
new UnprocessableError(SessionErrors.alreadyRevoked, {
|
|
58
51
|
i18nKey: "sessions.errors.alreadyRevoked",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import {
|
|
3
3
|
access,
|
|
4
4
|
createEntity,
|
|
@@ -64,4 +64,4 @@ export const userSessionEntity = createEntity({
|
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
export const userSessionTable =
|
|
67
|
+
export const userSessionTable = buildEntityTable("user_session", userSessionEntity);
|
|
@@ -5,10 +5,10 @@ import type {
|
|
|
5
5
|
SessionMetadata,
|
|
6
6
|
SessionRevoker,
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/api";
|
|
8
|
+
import { fetchOne, insertOne, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
8
9
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
9
10
|
import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
10
11
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
11
|
-
import { and, eq, isNull } from "drizzle-orm";
|
|
12
12
|
import { Temporal } from "temporal-polyfill";
|
|
13
13
|
import { DEFAULT_SESSION_EXPIRY_MS } from "./constants";
|
|
14
14
|
import { userSessionTable } from "./schema/user-session";
|
|
@@ -46,7 +46,7 @@ export function createSessionCallbacks(opts: SessionCallbacksOptions): SessionCa
|
|
|
46
46
|
const sid = generateId();
|
|
47
47
|
const now = Temporal.Now.instant();
|
|
48
48
|
const expiresAt = now.add({ milliseconds: ttlMs });
|
|
49
|
-
await db
|
|
49
|
+
await insertOne(db, userSessionTable, {
|
|
50
50
|
id: sid,
|
|
51
51
|
tenantId: user.tenantId,
|
|
52
52
|
userId: user.id,
|
|
@@ -64,23 +64,20 @@ export function createSessionCallbacks(opts: SessionCallbacksOptions): SessionCa
|
|
|
64
64
|
// original timestamp. Double-revoke races land here via logout +
|
|
65
65
|
// switch-tenant on the same sid. (Password-change uses a different
|
|
66
66
|
// callback — sessionMassRevoker — and isn't in scope for this guard.)
|
|
67
|
-
await
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
67
|
+
await updateMany(
|
|
68
|
+
db,
|
|
69
|
+
userSessionTable,
|
|
70
|
+
{ revokedAt: Temporal.Now.instant() },
|
|
71
|
+
{ id: sid, revokedAt: null },
|
|
72
|
+
);
|
|
71
73
|
},
|
|
72
74
|
|
|
73
75
|
async sessionChecker(sid: string, expectedUserId: string): Promise<AuthSessionStatus> {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
})
|
|
80
|
-
.from(userSessionTable)
|
|
81
|
-
.where(eq(userSessionTable.id, sid))
|
|
82
|
-
.limit(1);
|
|
83
|
-
const row = rows[0];
|
|
76
|
+
const row = await fetchOne<{
|
|
77
|
+
userId: string;
|
|
78
|
+
revokedAt: unknown;
|
|
79
|
+
expiresAt: { epochMilliseconds: number };
|
|
80
|
+
}>(db, userSessionTable, { id: sid });
|
|
84
81
|
if (!row) return "missing";
|
|
85
82
|
// Cross-user check: if the sid belongs to someone else, treat it
|
|
86
83
|
// identically to "missing" so a compromised sid paired with a valid
|
|
@@ -99,11 +96,12 @@ export function createSessionCallbacks(opts: SessionCallbacksOptions): SessionCa
|
|
|
99
96
|
async sessionMassRevoker(userId: string): Promise<number> {
|
|
100
97
|
// Count is accurate because we only touch live rows — a previously
|
|
101
98
|
// revoked row stays in its state and isn't double-counted.
|
|
102
|
-
const result = await
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
|
|
99
|
+
const result = await updateMany(
|
|
100
|
+
db,
|
|
101
|
+
userSessionTable,
|
|
102
|
+
{ revokedAt: Temporal.Now.instant() },
|
|
103
|
+
{ userId, revokedAt: null },
|
|
104
|
+
);
|
|
107
105
|
return result.length;
|
|
108
106
|
},
|
|
109
107
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// feature.ts contract tests for subscription-mollie.
|
|
2
2
|
|
|
3
|
-
import { describe, expect, test } from "
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
4
|
import { MOLLIE_PROVIDER_NAME, SUBSCRIPTION_MOLLIE_FEATURE } from "../constants";
|
|
5
5
|
import { createSubscriptionMollieFeature } from "../feature";
|
|
6
6
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// Unit-Tests (verify-webhook.test.ts) abgedeckt, aber die Verdrahtung
|
|
19
19
|
// von factory bis foundation-DB-row beweist nur dieser Test.
|
|
20
20
|
|
|
21
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
21
22
|
import {
|
|
22
23
|
billingFoundationFeature,
|
|
23
24
|
createSubscriptionWebhookHandler,
|
|
@@ -38,7 +39,6 @@ import type {
|
|
|
38
39
|
Subscription as MollieSubscription,
|
|
39
40
|
} from "@mollie/api-client";
|
|
40
41
|
import { Hono } from "hono";
|
|
41
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
42
42
|
import { createSubscriptionMollieFeature } from "../feature";
|
|
43
43
|
import type { MollieClientShape } from "../verify-webhook";
|
|
44
44
|
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// als minimal-mock-shape (`MollieClientShape`) injiziert; Plugin-
|
|
4
4
|
// Verhalten ist vom konkreten Mollie-SDK entkoppelt.
|
|
5
5
|
|
|
6
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
6
7
|
import {
|
|
7
8
|
SubscriptionEventTypes,
|
|
8
9
|
SubscriptionStatuses,
|
|
9
10
|
} from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
10
|
-
import { describe, expect, test, vi } from "vitest";
|
|
11
11
|
import {
|
|
12
12
|
extractMollieId,
|
|
13
13
|
type MollieClientShape,
|
|
@@ -61,18 +61,18 @@ function buildClient(
|
|
|
61
61
|
): MollieClientShape {
|
|
62
62
|
return {
|
|
63
63
|
payments: {
|
|
64
|
-
get:
|
|
64
|
+
get: mock(async () => {
|
|
65
65
|
if (overrides.paymentReject) throw overrides.paymentReject;
|
|
66
66
|
return overrides.paymentResolve ?? buildMockPayment();
|
|
67
67
|
}),
|
|
68
68
|
},
|
|
69
69
|
customerSubscriptions: {
|
|
70
|
-
get:
|
|
70
|
+
get: mock(async () => {
|
|
71
71
|
if (overrides.subReject) throw overrides.subReject;
|
|
72
72
|
return overrides.subResolve ?? buildMockSubscription();
|
|
73
73
|
}),
|
|
74
|
-
list:
|
|
75
|
-
create:
|
|
74
|
+
list: mock(async () => overrides.listResolve ?? []),
|
|
75
|
+
create: mock(
|
|
76
76
|
async () => overrides.createResolve ?? buildMockSubscription({ id: "sub_just_created" }),
|
|
77
77
|
),
|
|
78
78
|
},
|
|
@@ -197,7 +197,8 @@ describe("verifyAndParseMollieWebhook — mandate-setup-flow (= first-payment-pa
|
|
|
197
197
|
expect(event).not.toBeNull();
|
|
198
198
|
expect(event?.type).toBe(SubscriptionEventTypes.created);
|
|
199
199
|
expect(event?.providerSubscriptionId).toBe("sub_just_created");
|
|
200
|
-
expect(client.customerSubscriptions.create).
|
|
200
|
+
expect(client.customerSubscriptions.create).toHaveBeenCalledTimes(1);
|
|
201
|
+
expect(client.customerSubscriptions.create).toHaveBeenCalledWith("cst_test_001", {
|
|
201
202
|
amount: { currency: "EUR", value: "9.99" },
|
|
202
203
|
interval: "1 month",
|
|
203
204
|
description: "Pro-Abo monatlich",
|
|
@@ -250,7 +251,7 @@ describe("verifyAndParseMollieWebhook — mandate-setup-flow (= first-payment-pa
|
|
|
250
251
|
const event = await verify(client)("id=tr_upgrade", {});
|
|
251
252
|
|
|
252
253
|
expect(event?.providerSubscriptionId).toBe("sub_pro_new");
|
|
253
|
-
expect(client.customerSubscriptions.create).
|
|
254
|
+
expect(client.customerSubscriptions.create).toHaveBeenCalledTimes(1);
|
|
254
255
|
});
|
|
255
256
|
});
|
|
256
257
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// feature.ts contract tests for subscription-stripe.
|
|
2
2
|
|
|
3
|
-
import { describe, expect, test } from "
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
4
|
import { STRIPE_PROVIDER_NAME, StripeEventTypes, SUBSCRIPTION_STRIPE_FEATURE } from "../constants";
|
|
5
5
|
import { createSubscriptionStripeFeature } from "../feature";
|
|
6
6
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Unit-Tests für die Stripe-Plugin-Methoden (createCheckoutSession,
|
|
2
2
|
// createPortalSession, cancelSubscription). Stripe-SDK-calls werden via
|
|
3
|
-
//
|
|
3
|
+
// spyOn gemockt — wir testen unsere Mapping-Logik (Argumente die wir
|
|
4
4
|
// an Stripe schicken + Antwort-Parsing), NICHT Stripe selbst.
|
|
5
5
|
|
|
6
|
+
import { describe, expect, spyOn, test } from "bun:test";
|
|
6
7
|
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
8
|
import Stripe from "stripe";
|
|
8
|
-
import { describe, expect, test, vi } from "vitest";
|
|
9
9
|
import {
|
|
10
10
|
createStripeCancelSubscription,
|
|
11
11
|
createStripeCheckoutSession,
|
|
@@ -27,8 +27,7 @@ const stubCtx = {} as HandlerContext;
|
|
|
27
27
|
describe("createStripeCheckoutSession", () => {
|
|
28
28
|
test("ruft stripe.checkout.sessions.create mit mode=subscription + tenant-metadata", async () => {
|
|
29
29
|
const stripe = buildStripe();
|
|
30
|
-
const createMock =
|
|
31
|
-
.spyOn(stripe.checkout.sessions, "create")
|
|
30
|
+
const createMock = spyOn(stripe.checkout.sessions, "create")
|
|
32
31
|
// biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
|
|
33
32
|
.mockResolvedValue({ url: "https://checkout.stripe.com/c/pay/test" } as any);
|
|
34
33
|
|
|
@@ -41,7 +40,8 @@ describe("createStripeCheckoutSession", () => {
|
|
|
41
40
|
});
|
|
42
41
|
|
|
43
42
|
expect(result).toEqual({ url: "https://checkout.stripe.com/c/pay/test" });
|
|
44
|
-
expect(createMock).
|
|
43
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
44
|
+
expect(createMock).toHaveBeenCalledWith({
|
|
45
45
|
mode: "subscription",
|
|
46
46
|
line_items: [{ price: "price_pro_monthly", quantity: 1 }],
|
|
47
47
|
success_url: "https://example.com/success",
|
|
@@ -57,8 +57,7 @@ describe("createStripeCheckoutSession", () => {
|
|
|
57
57
|
|
|
58
58
|
test("passes existing customer-id wenn gesetzt (Plan-Wechsel-Flow)", async () => {
|
|
59
59
|
const stripe = buildStripe();
|
|
60
|
-
const createMock =
|
|
61
|
-
.spyOn(stripe.checkout.sessions, "create")
|
|
60
|
+
const createMock = spyOn(stripe.checkout.sessions, "create")
|
|
62
61
|
// biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
|
|
63
62
|
.mockResolvedValue({ url: "https://x" } as any);
|
|
64
63
|
|
|
@@ -78,7 +77,7 @@ describe("createStripeCheckoutSession", () => {
|
|
|
78
77
|
|
|
79
78
|
test("throws wenn Stripe keine url returnt (defensive — sollte nie passieren bei mode=subscription)", async () => {
|
|
80
79
|
const stripe = buildStripe();
|
|
81
|
-
|
|
80
|
+
spyOn(stripe.checkout.sessions, "create")
|
|
82
81
|
// biome-ignore lint/suspicious/noExplicitAny: SDK-Drift-Test
|
|
83
82
|
.mockResolvedValue({ url: null } as any);
|
|
84
83
|
|
|
@@ -99,7 +98,7 @@ describe("createStripeCheckoutSession", () => {
|
|
|
99
98
|
// throw kriegt + zur HTTP 500 mapped (transient — Provider/Stripe
|
|
100
99
|
// soll retried werden statt silent-success-mit-leerer-URL).
|
|
101
100
|
const stripe = buildStripe();
|
|
102
|
-
|
|
101
|
+
spyOn(stripe.checkout.sessions, "create").mockRejectedValue(
|
|
103
102
|
new Error("Stripe API: Internal server error"),
|
|
104
103
|
);
|
|
105
104
|
|
|
@@ -122,8 +121,7 @@ describe("createStripeCheckoutSession", () => {
|
|
|
122
121
|
describe("createStripePortalSession", () => {
|
|
123
122
|
test("ruft stripe.billingPortal.sessions.create mit customer + return_url", async () => {
|
|
124
123
|
const stripe = buildStripe();
|
|
125
|
-
const createMock =
|
|
126
|
-
.spyOn(stripe.billingPortal.sessions, "create")
|
|
124
|
+
const createMock = spyOn(stripe.billingPortal.sessions, "create")
|
|
127
125
|
// biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
|
|
128
126
|
.mockResolvedValue({ url: "https://billing.stripe.com/p/session/test" } as any);
|
|
129
127
|
|
|
@@ -134,7 +132,8 @@ describe("createStripePortalSession", () => {
|
|
|
134
132
|
});
|
|
135
133
|
|
|
136
134
|
expect(result).toEqual({ url: "https://billing.stripe.com/p/session/test" });
|
|
137
|
-
expect(createMock).
|
|
135
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(createMock).toHaveBeenCalledWith({
|
|
138
137
|
customer: "cus_001",
|
|
139
138
|
return_url: "https://example.com/return",
|
|
140
139
|
});
|
|
@@ -148,14 +147,14 @@ describe("createStripePortalSession", () => {
|
|
|
148
147
|
describe("createStripeCancelSubscription", () => {
|
|
149
148
|
test("ruft stripe.subscriptions.cancel mit subscription-id", async () => {
|
|
150
149
|
const stripe = buildStripe();
|
|
151
|
-
const cancelMock =
|
|
152
|
-
.spyOn(stripe.subscriptions, "cancel")
|
|
150
|
+
const cancelMock = spyOn(stripe.subscriptions, "cancel")
|
|
153
151
|
// biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
|
|
154
152
|
.mockResolvedValue({ id: "sub_001", status: "canceled" } as any);
|
|
155
153
|
|
|
156
154
|
const cancel = createStripeCancelSubscription(stripe);
|
|
157
155
|
await cancel(stubCtx, "sub_001");
|
|
158
156
|
|
|
159
|
-
expect(cancelMock).
|
|
157
|
+
expect(cancelMock).toHaveBeenCalledTimes(1);
|
|
158
|
+
expect(cancelMock).toHaveBeenCalledWith("sub_001");
|
|
160
159
|
});
|
|
161
160
|
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// Stripe-output liefert). Dieser Test fängt das Spalten-Mapping +
|
|
13
13
|
// Verdrahtungs-Bugs ab.
|
|
14
14
|
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
15
16
|
import {
|
|
16
17
|
billingFoundationFeature,
|
|
17
18
|
createSubscriptionWebhookHandler,
|
|
@@ -29,7 +30,6 @@ import {
|
|
|
29
30
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
30
31
|
import { Hono } from "hono";
|
|
31
32
|
import Stripe from "stripe";
|
|
32
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
33
33
|
import { createSubscriptionStripeFeature } from "../feature";
|
|
34
34
|
|
|
35
35
|
// =============================================================================
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
// events sind >100 Felder; full-fidelity-fixtures wären Maintenance-
|
|
8
8
|
// Aufwand ohne Test-Wert.
|
|
9
9
|
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
10
11
|
import {
|
|
11
12
|
SubscriptionEventTypes,
|
|
12
13
|
SubscriptionStatuses,
|
|
13
14
|
} from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
14
15
|
import Stripe from "stripe";
|
|
15
|
-
import { describe, expect, test } from "vitest";
|
|
16
16
|
import {
|
|
17
17
|
mapStripeEventType,
|
|
18
18
|
mapStripeStatus,
|
|
@@ -83,8 +83,8 @@ function buildSubscriptionEvent(overrides: {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/** Erstellt einen valid Stripe-signed-Header für ein gegebenes payload. */
|
|
86
|
-
function signEvent(payload: string, secret = TEST_SECRET): string {
|
|
87
|
-
return stripeForFixtures.webhooks.
|
|
86
|
+
async function signEvent(payload: string, secret = TEST_SECRET): Promise<string> {
|
|
87
|
+
return stripeForFixtures.webhooks.generateTestHeaderStringAsync({
|
|
88
88
|
payload,
|
|
89
89
|
secret,
|
|
90
90
|
});
|
|
@@ -102,7 +102,7 @@ describe("verifyAndParseStripeWebhook — sig-verify", () => {
|
|
|
102
102
|
|
|
103
103
|
test("happy path: valid sig + bekannter event-type → SubscriptionEvent", async () => {
|
|
104
104
|
const payload = JSON.stringify(buildSubscriptionEvent({}));
|
|
105
|
-
const sig = signEvent(payload);
|
|
105
|
+
const sig = await signEvent(payload);
|
|
106
106
|
|
|
107
107
|
const event = await verify(payload, { "stripe-signature": sig });
|
|
108
108
|
expect(event).not.toBeNull();
|
|
@@ -120,7 +120,7 @@ describe("verifyAndParseStripeWebhook — sig-verify", () => {
|
|
|
120
120
|
|
|
121
121
|
test("wrong secret → sig-verify failed → throws", async () => {
|
|
122
122
|
const payload = JSON.stringify(buildSubscriptionEvent({}));
|
|
123
|
-
const sig = signEvent(payload, "whsec_wrong_secret");
|
|
123
|
+
const sig = await signEvent(payload, "whsec_wrong_secret");
|
|
124
124
|
await expect(verify(payload, { "stripe-signature": sig })).rejects.toThrow(
|
|
125
125
|
/signature verify failed/,
|
|
126
126
|
);
|
|
@@ -128,7 +128,7 @@ describe("verifyAndParseStripeWebhook — sig-verify", () => {
|
|
|
128
128
|
|
|
129
129
|
test("modified body → sig-verify failed (Replay-Protection)", async () => {
|
|
130
130
|
const original = JSON.stringify(buildSubscriptionEvent({}));
|
|
131
|
-
const sig = signEvent(original);
|
|
131
|
+
const sig = await signEvent(original);
|
|
132
132
|
// Tamper with body — Stripe-sig matched die exakten bytes.
|
|
133
133
|
const tampered = original.replace("tenant-test-1", "tenant-attacker");
|
|
134
134
|
await expect(verify(tampered, { "stripe-signature": sig })).rejects.toThrow(
|
|
@@ -151,7 +151,7 @@ describe("verifyAndParseStripeWebhook — event-filter", () => {
|
|
|
151
151
|
// customer.created ist gültiger Stripe-event aber nicht in unserer
|
|
152
152
|
// 5-types-Whitelist.
|
|
153
153
|
const payload = JSON.stringify(buildSubscriptionEvent({ eventType: "customer.created" }));
|
|
154
|
-
const sig = signEvent(payload);
|
|
154
|
+
const sig = await signEvent(payload);
|
|
155
155
|
const event = await verify(payload, { "stripe-signature": sig });
|
|
156
156
|
expect(event).toBeNull();
|
|
157
157
|
});
|
|
@@ -160,7 +160,7 @@ describe("verifyAndParseStripeWebhook — event-filter", () => {
|
|
|
160
160
|
const payload = JSON.stringify(
|
|
161
161
|
buildSubscriptionEvent({ eventType: "customer.subscription.updated" }),
|
|
162
162
|
);
|
|
163
|
-
const sig = signEvent(payload);
|
|
163
|
+
const sig = await signEvent(payload);
|
|
164
164
|
const event = await verify(payload, { "stripe-signature": sig });
|
|
165
165
|
expect(event?.type).toBe(SubscriptionEventTypes.updated);
|
|
166
166
|
});
|
|
@@ -172,7 +172,7 @@ describe("verifyAndParseStripeWebhook — event-filter", () => {
|
|
|
172
172
|
status: "canceled",
|
|
173
173
|
}),
|
|
174
174
|
);
|
|
175
|
-
const sig = signEvent(payload);
|
|
175
|
+
const sig = await signEvent(payload);
|
|
176
176
|
const event = await verify(payload, { "stripe-signature": sig });
|
|
177
177
|
expect(event?.type).toBe(SubscriptionEventTypes.canceled);
|
|
178
178
|
expect(event?.status).toBe(SubscriptionStatuses.canceled);
|
|
@@ -200,7 +200,7 @@ describe("verifyAndParseStripeWebhook — event-filter", () => {
|
|
|
200
200
|
},
|
|
201
201
|
};
|
|
202
202
|
const payload = JSON.stringify(ev);
|
|
203
|
-
const sig = signEvent(payload);
|
|
203
|
+
const sig = await signEvent(payload);
|
|
204
204
|
const event = await verify(payload, { "stripe-signature": sig });
|
|
205
205
|
expect(event).toBeNull();
|
|
206
206
|
});
|
|
@@ -221,21 +221,21 @@ describe("verifyAndParseStripeWebhook — tenant-resolution + price-to-tier", ()
|
|
|
221
221
|
// @ts-expect-error — entferne metadata für Test
|
|
222
222
|
ev.data.object.metadata = {};
|
|
223
223
|
const payload = JSON.stringify(ev);
|
|
224
|
-
const sig = signEvent(payload);
|
|
224
|
+
const sig = await signEvent(payload);
|
|
225
225
|
const event = await verify(payload, { "stripe-signature": sig });
|
|
226
226
|
expect(event).toBeNull();
|
|
227
227
|
});
|
|
228
228
|
|
|
229
229
|
test("price-id im Mapping → korrekter tier-Wert", async () => {
|
|
230
230
|
const payload = JSON.stringify(buildSubscriptionEvent({ priceId: "price_business_yearly" }));
|
|
231
|
-
const sig = signEvent(payload);
|
|
231
|
+
const sig = await signEvent(payload);
|
|
232
232
|
const event = await verify(payload, { "stripe-signature": sig });
|
|
233
233
|
expect(event?.tier).toBe("business");
|
|
234
234
|
});
|
|
235
235
|
|
|
236
236
|
test("price-id NICHT im Mapping → null", async () => {
|
|
237
237
|
const payload = JSON.stringify(buildSubscriptionEvent({ priceId: "price_unknown_xyz" }));
|
|
238
|
-
const sig = signEvent(payload);
|
|
238
|
+
const sig = await signEvent(payload);
|
|
239
239
|
const event = await verify(payload, { "stripe-signature": sig });
|
|
240
240
|
expect(event).toBeNull();
|
|
241
241
|
});
|
|
@@ -243,7 +243,7 @@ describe("verifyAndParseStripeWebhook — tenant-resolution + price-to-tier", ()
|
|
|
243
243
|
test("currentPeriodEnd wird aus subscription.items[0].current_period_end (Unix-sec) zu ISO konvertiert", async () => {
|
|
244
244
|
const periodEndUnix = 1_780_000_000;
|
|
245
245
|
const payload = JSON.stringify(buildSubscriptionEvent({ currentPeriodEndUnix: periodEndUnix }));
|
|
246
|
-
const sig = signEvent(payload);
|
|
246
|
+
const sig = await signEvent(payload);
|
|
247
247
|
const event = await verify(payload, { "stripe-signature": sig });
|
|
248
248
|
// 1_780_000_000 sec = 2026-05-28T20:26:40Z (in ms: 1.78e12)
|
|
249
249
|
// Temporal.Instant.toString() droppt Trailing-Zeros — keine .000Z
|