@cosmicdrift/kumiko-bundled-features 0.2.2 → 0.3.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/CHANGELOG.md +91 -0
- package/package.json +22 -13
- package/src/auth-email-password/auth-user-row.ts +6 -0
- package/src/auth-email-password/constants.ts +11 -0
- package/src/auth-email-password/handlers/change-password.write.ts +1 -1
- package/src/auth-email-password/handlers/confirm-token-flow.ts +1 -1
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +7 -7
- package/src/auth-email-password/handlers/invite-accept.write.ts +7 -6
- package/src/auth-email-password/handlers/invite-create.write.ts +3 -3
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -4
- package/src/auth-email-password/handlers/login.write.ts +32 -2
- package/src/auth-email-password/handlers/logout.write.ts +2 -2
- package/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
- package/src/auth-email-password/i18n.ts +4 -0
- package/src/auth-email-password/web/auth-client.ts +1 -1
- package/src/billing-foundation/events.ts +1 -1
- package/src/billing-foundation/feature.ts +44 -47
- package/src/billing-foundation/handlers/create-portal-session.write.ts +3 -3
- package/src/billing-foundation/handlers/process-event.write.ts +3 -3
- package/src/billing-foundation/projection.ts +1 -1
- package/src/billing-foundation/webhook-handler.ts +1 -1
- package/src/cap-counter/constants.ts +1 -1
- package/src/cap-counter/enforce-cap.ts +1 -1
- package/src/cap-counter/feature.ts +3 -7
- package/src/cap-counter/handlers/get-counter.query.ts +1 -1
- package/src/cap-counter/handlers/increment-rolling.write.ts +2 -2
- package/src/cap-counter/handlers/increment.write.ts +3 -3
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
- package/src/channel-email/email-channel.ts +1 -1
- package/src/channel-email/types.ts +1 -1
- package/src/compliance-profiles/README.md +88 -0
- package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +308 -0
- package/src/compliance-profiles/__tests__/seeding.integration.ts +93 -0
- package/src/compliance-profiles/feature.ts +51 -0
- package/src/compliance-profiles/handlers/for-tenant.query.ts +64 -0
- package/src/compliance-profiles/handlers/list-profiles.query.ts +44 -0
- package/src/compliance-profiles/handlers/needs-profile.query.ts +56 -0
- package/src/compliance-profiles/handlers/set-profile.write.ts +144 -0
- package/src/compliance-profiles/handlers/sub-processors.query.ts +43 -0
- package/src/compliance-profiles/index.ts +6 -0
- package/src/compliance-profiles/resolve-for-tenant.ts +63 -0
- package/src/compliance-profiles/schema/profile-selection.ts +52 -0
- package/src/compliance-profiles/seeding.ts +96 -0
- package/src/config/resolver.ts +1 -1
- package/src/data-retention/__tests__/data-retention.integration.ts +49 -0
- package/src/data-retention/__tests__/keep-for.test.ts +77 -0
- package/src/data-retention/__tests__/override-schema.test.ts +96 -0
- package/src/data-retention/__tests__/policy-for.integration.ts +172 -0
- package/src/data-retention/__tests__/resolver.test.ts +201 -0
- package/src/data-retention/_internal/parse-override.ts +34 -0
- package/src/data-retention/feature.ts +57 -0
- package/src/data-retention/handlers/policy-for.query.ts +57 -0
- package/src/data-retention/index.ts +18 -0
- package/src/data-retention/keep-for.ts +75 -0
- package/src/data-retention/override-schema.ts +37 -0
- package/src/data-retention/presets.ts +72 -0
- package/src/data-retention/resolve-for-tenant.ts +50 -0
- package/src/data-retention/resolver.ts +107 -0
- package/src/data-retention/schema/tenant-retention-override.ts +47 -0
- package/src/delivery/feature.ts +1 -1
- package/src/delivery/testing.ts +1 -2
- package/src/delivery/upsert-preference.ts +1 -1
- package/src/feature-toggles/feature.ts +1 -1
- package/src/feature-toggles/handlers/list.query.ts +1 -1
- package/src/feature-toggles/handlers/registered.query.ts +9 -2
- package/src/feature-toggles/handlers/set.write.ts +3 -3
- package/src/file-foundation/feature.ts +44 -4
- package/src/file-foundation/index.ts +1 -0
- package/src/file-provider-inmemory/feature.ts +6 -3
- package/src/file-provider-s3/feature.ts +10 -12
- package/src/files/README.md +50 -0
- package/src/files/__tests__/files.integration.ts +157 -0
- package/src/files/feature.ts +34 -0
- package/src/files/index.ts +1 -0
- package/src/files/schema/file-ref.ts +58 -0
- package/src/files-provider-s3/s3-provider.ts +90 -1
- package/src/jobs/handlers/list.query.ts +3 -3
- package/src/jobs/handlers/trigger.write.ts +1 -1
- package/src/legal-pages/constants.ts +1 -0
- package/src/legal-pages/web/client-plugin.ts +42 -0
- package/src/legal-pages/web/index.ts +4 -0
- package/src/mail-foundation/feature.ts +1 -1
- package/src/mail-transport-smtp/feature.ts +2 -2
- package/src/renderer-simple/simple-renderer.ts +1 -1
- package/src/secrets/__tests__/require-secrets-context.test.ts +81 -0
- package/src/secrets/feature.ts +10 -6
- package/src/secrets/handlers/rotate.job.ts +2 -2
- package/src/sessions/constants.ts +4 -0
- package/src/sessions/feature.ts +3 -0
- package/src/sessions/handlers/cleanup.job.ts +2 -2
- package/src/sessions/handlers/revoke-all-for-user.write.ts +42 -0
- package/src/step-dispatcher/feature.ts +62 -0
- package/src/step-dispatcher/index.ts +16 -0
- package/src/step-dispatcher/mail-runner.ts +32 -0
- package/src/step-dispatcher/webhook-runner.ts +67 -0
- package/src/subscription-mollie/plugin-methods.ts +1 -1
- package/src/subscription-mollie/verify-webhook.ts +9 -5
- package/src/subscription-stripe/verify-webhook.ts +3 -3
- package/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
- package/src/tenant/handlers/cancel-invitation.write.ts +1 -1
- package/src/tenant/handlers/remove-member.write.ts +1 -1
- package/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
- package/src/tenant/handlers/update-member-roles.write.ts +3 -3
- package/src/text-content/constants.ts +2 -0
- package/src/text-content/feature.ts +20 -4
- package/src/text-content/handlers/by-tenant.query.ts +56 -0
- package/src/text-content/handlers/set.write.ts +1 -1
- package/src/text-content/web/client-plugin.ts +113 -0
- package/src/text-content/web/index.ts +8 -0
- package/src/tier-engine/__tests__/auto-default-tier.integration.ts +118 -0
- package/src/tier-engine/feature.ts +23 -13
- package/src/user/__tests__/user-status.test.ts +39 -0
- package/src/user/handlers/find-for-auth.query.ts +1 -1
- package/src/user/index.ts +11 -1
- package/src/user/schema/user.ts +76 -0
- package/src/user/seeding.ts +2 -2
- package/src/user-data-rights/COMPLIANCE.md +182 -0
- package/src/user-data-rights/README.md +109 -0
- package/src/user-data-rights/__tests__/audit-log.integration.ts +199 -0
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +349 -0
- package/src/user-data-rights/__tests__/download.integration.ts +565 -0
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +244 -0
- package/src/user-data-rights/__tests__/export-job-schema.test.ts +163 -0
- package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +30 -0
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +370 -0
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +179 -0
- package/src/user-data-rights/__tests__/request-export.integration.ts +269 -0
- package/src/user-data-rights/__tests__/restriction-flow.integration.ts +309 -0
- package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +1124 -0
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +703 -0
- package/src/user-data-rights/__tests__/run-user-export.integration.ts +291 -0
- package/src/user-data-rights/__tests__/token-helpers.test.ts +63 -0
- package/src/user-data-rights/__tests__/user-data-rights.integration.ts +57 -0
- package/src/user-data-rights/__tests__/zip-path.test.ts +119 -0
- package/src/user-data-rights/audit-download.ts +125 -0
- package/src/user-data-rights/feature.ts +310 -0
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +84 -0
- package/src/user-data-rights/handlers/download-by-job.query.ts +206 -0
- package/src/user-data-rights/handlers/download-by-token.query.ts +255 -0
- package/src/user-data-rights/handlers/export-status.query.ts +76 -0
- package/src/user-data-rights/handlers/lift-restriction.write.ts +68 -0
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +53 -0
- package/src/user-data-rights/handlers/my-audit-log.query.ts +63 -0
- package/src/user-data-rights/handlers/request-deletion.write.ts +123 -0
- package/src/user-data-rights/handlers/request-export.write.ts +155 -0
- package/src/user-data-rights/handlers/restrict-account.write.ts +81 -0
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +61 -0
- package/src/user-data-rights/i18n.ts +37 -0
- package/src/user-data-rights/index.ts +19 -0
- package/src/user-data-rights/run-export-jobs.ts +878 -0
- package/src/user-data-rights/run-forget-cleanup.ts +333 -0
- package/src/user-data-rights/run-user-export.ts +211 -0
- package/src/user-data-rights/schema/download-attempt.ts +37 -0
- package/src/user-data-rights/schema/download-token.ts +111 -0
- package/src/user-data-rights/schema/export-job.ts +166 -0
- package/src/user-data-rights/token-helpers.ts +67 -0
- package/src/user-data-rights/zip-path.ts +94 -0
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +337 -0
- package/src/user-data-rights-defaults/feature.ts +40 -0
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +109 -0
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +91 -0
- package/src/user-data-rights-defaults/index.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
# @cosmicdrift/kumiko-bundled-features
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0.3.0 bringt zwei neue Subsysteme (Step-Engine Tier-3 + Visual-Tree) plus
|
|
8
|
+
eine AST-Codemod-Pipeline als Vorarbeit für den L2-AI-Layer.
|
|
9
|
+
|
|
10
|
+
### Breaking Changes
|
|
11
|
+
|
|
12
|
+
- `skipTransitionGuard` → `unsafeSkipTransitionGuard` (Rename in
|
|
13
|
+
feature-ast + engine). Der `unsafe`-Prefix macht die Tragweite des
|
|
14
|
+
Casts sichtbar und ist konsistent zur `unsafeProjectionUpsert`- und
|
|
15
|
+
`r.rawTable`-Konvention. Migration: 1:1-Ersetzung, keine Verhaltens-Änderung.
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- **Step-Engine M.4 — Tier-3 Workflow-Engine.** Neue Step-Vocabulary
|
|
20
|
+
`wait`, `waitForEvent`, `retry` ermöglicht persistierte Long-Running-Flows
|
|
21
|
+
über Job-Boundaries hinweg. Q7 Snapshot-at-Start hängt jedem Step-Run
|
|
22
|
+
einen SHA-256-Fingerprint des Aggregat-Zustands an, sodass Replays
|
|
23
|
+
deterministisch gegen den ursprünglichen Eingangszustand laufen.
|
|
24
|
+
- **Visual-Tree V.1.x — Tree-API + Editor-Panel.** Neue `VisualTree`-
|
|
25
|
+
Component plus TreeProvider-Pattern; erste TreeProviders für
|
|
26
|
+
`text-content` und `legal-pages` (CMS-light + Impressum/Privacy).
|
|
27
|
+
Fundament für den späteren No-Code-Designer (~3000 LOC, 98 Tests).
|
|
28
|
+
- **Codemod-Pipeline.** AST-basierte Patcher-Module für strukturelle
|
|
29
|
+
Feature-Edits — wird vom kommenden L2-AI-Layer als Tool-Surface
|
|
30
|
+
verwendet, ist aber eigenständig nutzbar für ts-morph-style Migrationen.
|
|
31
|
+
- **user-data-rights Sample-Recipe.** DSGVO Art. 15/17/18/20 vollständig
|
|
32
|
+
als Sample-Recipe (`samples/recipes/`) inklusive README — zeigt die
|
|
33
|
+
Export- und Forget-Pipeline gegen den `compliance-profiles`-Default
|
|
34
|
+
(`eu-dsgvo`).
|
|
35
|
+
|
|
36
|
+
### Fixes
|
|
37
|
+
|
|
38
|
+
- `tier-engine`: auto-default-tier-Hook benutzt jetzt `ctx.db.raw` für
|
|
39
|
+
Event-Store-Operationen (#37, vorher: stiller Bug, 22 Tage live).
|
|
40
|
+
- `engine`: unsafe-projection-upsert nutzt `as never` statt `as any` —
|
|
41
|
+
schmaler Cast-Surface, weniger Compiler-Knebel.
|
|
42
|
+
- `visual-tree`: runtime-isolation marker für client-konsumierte Files,
|
|
43
|
+
damit der Multi-Entry-Build den richtigen Bundle-Split bekommt.
|
|
44
|
+
- `feature-ast`: vollständiger `unsafeSkipTransitionGuard`-Rename (war
|
|
45
|
+
in zwei Modulen noch der alte Name).
|
|
46
|
+
- `framework`: Error-Reasons + `noConsole`-Lint + No-Date-API-Guard
|
|
47
|
+
wieder push-ready.
|
|
48
|
+
|
|
49
|
+
### Library-Updates
|
|
50
|
+
|
|
51
|
+
hono 4.12, jose 6.2, stripe 22.1, meilisearch 0.58, marked 18,
|
|
52
|
+
bun-types 1.3.13, lucide-react 1.14, bullmq 5.76, ioredis 5.10,
|
|
53
|
+
i18next 26.0, react + radix-ui-primitives auf aktuelle Minors.
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- Updated dependencies
|
|
58
|
+
- @cosmicdrift/kumiko-framework@0.3.0
|
|
59
|
+
- @cosmicdrift/kumiko-dispatcher-live@0.3.0
|
|
60
|
+
- @cosmicdrift/kumiko-renderer@0.3.0
|
|
61
|
+
- @cosmicdrift/kumiko-renderer-web@0.3.0
|
|
62
|
+
|
|
63
|
+
## 0.2.3
|
|
64
|
+
|
|
65
|
+
### Patch Changes
|
|
66
|
+
|
|
67
|
+
- 1dbd038: Fix `db.execute is not a function` crash in `createTierEngineFeature`'s
|
|
68
|
+
auto-default-tier postSave-hook when called via the dispatcher path
|
|
69
|
+
(`tenant:write:create`). The hook used `ctx.db as DbConnection` — a
|
|
70
|
+
type-lie. AppContext.db in the inTransaction-phase is a TenantDb, which
|
|
71
|
+
exposes select/insert/update/delete but not execute(). The event-store-
|
|
72
|
+
append (event-store.ts:102) calls `db.execute(sql\`SELECT pg_notify(...)\`)`,
|
|
73
|
+
which crashed at runtime.
|
|
74
|
+
|
|
75
|
+
Fix: typeguard via `if (!("raw" in ctx.db)) return` then use `ctx.db.raw
|
|
76
|
+
as DbConnection` (pattern matched signup-confirm.write.ts:107).
|
|
77
|
+
|
|
78
|
+
Plus: regression integration-test in `tier-engine/__tests__/auto-default-
|
|
79
|
+
tier.integration.ts` covering the dispatcher path (sysadmin →
|
|
80
|
+
tenant:write:create → tier_assignments-row + idempotency on tenant-update).
|
|
81
|
+
|
|
82
|
+
**Known production gap (separate from this fix):** Self-Signup goes through
|
|
83
|
+
`provisionSignupAccount → seedTenant` (event-store-direct), which bypasses
|
|
84
|
+
the dispatcher → postSave-hooks never fire in production self-signup. This
|
|
85
|
+
fix makes the dispatcher path coherent. Real-signup auto-default needs
|
|
86
|
+
follow-up work (either seedTenant fires hooks or signup-confirm calls
|
|
87
|
+
explicit seed-helpers).
|
|
88
|
+
|
|
89
|
+
- @cosmicdrift/kumiko-framework@0.2.3
|
|
90
|
+
- @cosmicdrift/kumiko-dispatcher-live@0.2.3
|
|
91
|
+
- @cosmicdrift/kumiko-renderer@0.2.3
|
|
92
|
+
- @cosmicdrift/kumiko-renderer-web@0.2.3
|
|
93
|
+
|
|
3
94
|
## 0.2.2
|
|
4
95
|
|
|
5
96
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
|
21
21
|
"./audit": "./src/audit/index.ts",
|
|
22
|
+
"./compliance-profiles": "./src/compliance-profiles/index.ts",
|
|
22
23
|
"./config": "./src/config/index.ts",
|
|
24
|
+
"./data-retention": "./src/data-retention/index.ts",
|
|
23
25
|
"./jobs": "./src/jobs/index.ts",
|
|
24
26
|
"./tier-engine": "./src/tier-engine/index.ts",
|
|
25
27
|
"./cap-counter": "./src/cap-counter/index.ts",
|
|
@@ -33,6 +35,9 @@
|
|
|
33
35
|
"./file-foundation": "./src/file-foundation/index.ts",
|
|
34
36
|
"./file-provider-s3": "./src/file-provider-s3/index.ts",
|
|
35
37
|
"./file-provider-inmemory": "./src/file-provider-inmemory/index.ts",
|
|
38
|
+
"./files": "./src/files/index.ts",
|
|
39
|
+
"./user-data-rights": "./src/user-data-rights/index.ts",
|
|
40
|
+
"./user-data-rights-defaults": "./src/user-data-rights-defaults/index.ts",
|
|
36
41
|
"./tenant": "./src/tenant/index.ts",
|
|
37
42
|
"./tenant/constants": "./src/tenant/constants.ts",
|
|
38
43
|
"./tenant/seeding": "./src/tenant/seeding.ts",
|
|
@@ -58,25 +63,29 @@
|
|
|
58
63
|
"./feature-toggles": "./src/feature-toggles/index.ts",
|
|
59
64
|
"./text-content": "./src/text-content/index.ts",
|
|
60
65
|
"./text-content/seeding": "./src/text-content/seeding.ts",
|
|
61
|
-
"./
|
|
66
|
+
"./text-content/web": "./src/text-content/web/index.ts",
|
|
67
|
+
"./legal-pages": "./src/legal-pages/index.ts",
|
|
68
|
+
"./legal-pages/web": "./src/legal-pages/web/index.ts",
|
|
69
|
+
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
62
70
|
},
|
|
63
71
|
"dependencies": {
|
|
64
|
-
"@aws-sdk/client-s3": "^3.
|
|
65
|
-
"@aws-sdk/
|
|
66
|
-
"@
|
|
67
|
-
"@cosmicdrift/kumiko-
|
|
68
|
-
"@cosmicdrift/kumiko-
|
|
69
|
-
"@cosmicdrift/kumiko-renderer
|
|
72
|
+
"@aws-sdk/client-s3": "^3.1045.0",
|
|
73
|
+
"@aws-sdk/lib-storage": "^3.1045.0",
|
|
74
|
+
"@aws-sdk/s3-request-presigner": "^3.1045.0",
|
|
75
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.3.0",
|
|
76
|
+
"@cosmicdrift/kumiko-framework": "0.3.0",
|
|
77
|
+
"@cosmicdrift/kumiko-renderer": "0.3.0",
|
|
78
|
+
"@cosmicdrift/kumiko-renderer-web": "0.3.0",
|
|
70
79
|
"@mollie/api-client": "^4.5.0",
|
|
71
80
|
"@node-rs/argon2": "^2.0.2",
|
|
72
81
|
"@types/nodemailer": "^8.0.0",
|
|
73
82
|
"clsx": "^2.1.1",
|
|
74
|
-
"lucide-react": "^1.
|
|
75
|
-
"marked": "^
|
|
83
|
+
"lucide-react": "^1.14.0",
|
|
84
|
+
"marked": "^18.0.3",
|
|
76
85
|
"nodemailer": "^8.0.7",
|
|
77
|
-
"react": "^19.2.
|
|
78
|
-
"stripe": "^22.1.
|
|
79
|
-
"tailwind-merge": "^3.0
|
|
86
|
+
"react": "^19.2.6",
|
|
87
|
+
"stripe": "^22.1.1",
|
|
88
|
+
"tailwind-merge": "^3.6.0"
|
|
80
89
|
},
|
|
81
90
|
"publishConfig": {
|
|
82
91
|
"registry": "https://registry.npmjs.org",
|
|
@@ -21,6 +21,12 @@ export type AuthUserRow = {
|
|
|
21
21
|
// roles gelten (z.B. SystemAdmin, BillingAdmin). Caller deserialisiert via
|
|
22
22
|
// parseRoles() vor dem Merge in die Session.
|
|
23
23
|
readonly roles?: string | null;
|
|
24
|
+
// user.status (S2.U1) — "active" | "restricted" | "deletion_requested" |
|
|
25
|
+
// "deleted". Login.write.ts blockt Restricted (DSGVO Art. 18) sowie
|
|
26
|
+
// DeletionRequested + Deleted. Untyped string hier weil die Quelle
|
|
27
|
+
// user-feature-internes Enum ist; auth importiert den Constants-Wert
|
|
28
|
+
// an der Verwendungsstelle.
|
|
29
|
+
readonly status?: string | null;
|
|
24
30
|
};
|
|
25
31
|
|
|
26
32
|
// Returns the narrowed row or null — mirrors findForAuth's contract where
|
|
@@ -73,6 +73,17 @@ export const AuthErrors = {
|
|
|
73
73
|
// deliberate enumeration trade-off: the lockout event itself is already
|
|
74
74
|
// observable to the attacker, and legit users benefit from a clear signal.
|
|
75
75
|
accountLocked: "account_locked",
|
|
76
|
+
// S2.U6 (DSGVO Art. 18) — Account ist im Restricted-Status. Login wird
|
|
77
|
+
// explicit verweigert mit eigenem Code (nicht zu invalid_credentials
|
|
78
|
+
// collapsen) damit UI sagen kann "Account ist aktuell pausiert, hier
|
|
79
|
+
// klicken zum Aufheben". Enumeration-leak akzeptiert: Restriction ist
|
|
80
|
+
// user-initiiert, der User weiss dass sein Konto restricted ist.
|
|
81
|
+
accountRestricted: "account_restricted",
|
|
82
|
+
// Account ist im DeletionRequested- oder Deleted-Status. Anders als
|
|
83
|
+
// Restricted ist das nicht reversibel via Login → wir collapsen auf
|
|
84
|
+
// invalid_credentials damit Forget-Pfad nicht via Login enumerierbar
|
|
85
|
+
// wird (User der "Konto loeschen" geklickt hat soll nicht erneut sehen
|
|
86
|
+
// dass die Email-Adresse noch in der DB existiert).
|
|
76
87
|
} as const;
|
|
77
88
|
|
|
78
89
|
// Account-lockout defaults — overridable via
|
|
@@ -30,7 +30,7 @@ export const changePasswordWrite = defineWriteHandler({
|
|
|
30
30
|
// Load self with passwordHash — only visible to the privileged caller.
|
|
31
31
|
const me = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
32
32
|
id: event.user.id,
|
|
33
|
-
})) as { id: number; passwordHash: string | null; version: number } | null;
|
|
33
|
+
})) as { id: number; passwordHash: string | null; version: number } | null; // @cast-boundary db-runner
|
|
34
34
|
|
|
35
35
|
if (!me?.passwordHash) {
|
|
36
36
|
return invalidCredentials();
|
|
@@ -152,7 +152,7 @@ async function resolveStreamTenants(
|
|
|
152
152
|
): Promise<readonly TenantId[]> {
|
|
153
153
|
const memberships = (await ctx.queryAs(systemUser, "tenant:query:memberships", {
|
|
154
154
|
userId: me.id,
|
|
155
|
-
})) as Array<{ tenantId: TenantId }>;
|
|
155
|
+
})) as Array<{ tenantId: TenantId }>; // @cast-boundary db-runner
|
|
156
156
|
return orderTenantsByPreference(memberships, me.lastActiveTenantId);
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -114,10 +114,10 @@ export function createInviteAcceptWithLoginHandler() {
|
|
|
114
114
|
if (!invitation || invitation["status"] !== INVITATION_STATUS.pending)
|
|
115
115
|
return invalidInviteToken();
|
|
116
116
|
|
|
117
|
-
const invitationTenantId = invitation["tenantId"] as TenantId;
|
|
118
|
-
const invitationEmail = invitation["email"] as string;
|
|
119
|
-
const invitationRole = invitation["role"] as string;
|
|
120
|
-
const invitationVersion = invitation["version"] as number;
|
|
117
|
+
const invitationTenantId = invitation["tenantId"] as TenantId; // @cast-boundary db-row
|
|
118
|
+
const invitationEmail = invitation["email"] as string; // @cast-boundary db-row
|
|
119
|
+
const invitationRole = invitation["role"] as string; // @cast-boundary db-row
|
|
120
|
+
const invitationVersion = invitation["version"] as number; // @cast-boundary db-row
|
|
121
121
|
|
|
122
122
|
// Email-Match vom User-Input (nicht aus session — User ist anon)
|
|
123
123
|
if (event.payload.email.toLowerCase() !== invitationEmail) {
|
|
@@ -134,19 +134,19 @@ export function createInviteAcceptWithLoginHandler() {
|
|
|
134
134
|
const userRow = await fetchOne(ctx.db.raw, userTable, eq(userTable.email, invitationEmail));
|
|
135
135
|
if (!userRow?.["passwordHash"]) return invalidInviteToken();
|
|
136
136
|
const passwordValid = await verifyPassword(
|
|
137
|
-
userRow["passwordHash"] as string,
|
|
137
|
+
userRow["passwordHash"] as string, // @cast-boundary db-row
|
|
138
138
|
event.payload.password,
|
|
139
139
|
);
|
|
140
140
|
if (!passwordValid) return invalidInviteToken();
|
|
141
141
|
|
|
142
|
-
const userId = userRow["id"] as string;
|
|
142
|
+
const userId = userRow["id"] as string; // @cast-boundary db-row
|
|
143
143
|
|
|
144
144
|
// Already-Member-Check (idempotent)
|
|
145
145
|
const memberships = (await ctx.queryAs(
|
|
146
146
|
createSystemUser(invitationTenantId),
|
|
147
147
|
"tenant:query:memberships",
|
|
148
148
|
{ userId },
|
|
149
|
-
)) as Array<{ tenantId: string }>;
|
|
149
|
+
)) as Array<{ tenantId: string }>; // @cast-boundary db-row
|
|
150
150
|
const alreadyMember = memberships.some((m) => m.tenantId === invitationTenantId);
|
|
151
151
|
|
|
152
152
|
// @cast-boundary db-runner — TenantDb.raw is DbRunner
|
|
@@ -107,16 +107,17 @@ export function createInviteAcceptHandler() {
|
|
|
107
107
|
if (!invitation || invitation["status"] !== INVITATION_STATUS.pending)
|
|
108
108
|
return invalidInviteToken();
|
|
109
109
|
|
|
110
|
-
const invitationTenantId = invitation["tenantId"] as TenantId;
|
|
111
|
-
const invitationEmail = invitation["email"] as string;
|
|
112
|
-
const invitationRole = invitation["role"] as string;
|
|
113
|
-
const invitationVersion = invitation["version"] as number;
|
|
110
|
+
const invitationTenantId = invitation["tenantId"] as TenantId; // @cast-boundary db-row
|
|
111
|
+
const invitationEmail = invitation["email"] as string; // @cast-boundary db-row
|
|
112
|
+
const invitationRole = invitation["role"] as string; // @cast-boundary db-row
|
|
113
|
+
const invitationVersion = invitation["version"] as number; // @cast-boundary db-row
|
|
114
114
|
|
|
115
115
|
// Email-Match: User muss mit der eingeladenen Email matchen.
|
|
116
116
|
// Sonst kann ein Angreifer mit Zugriff zur invitee-Mail seinen
|
|
117
117
|
// eigenen Account dem Tenant zuschlagen.
|
|
118
118
|
const userRow = await fetchOne(ctx.db.raw, userTable, eq(userTable.id, event.user.id));
|
|
119
|
-
|
|
119
|
+
const userEmail = userRow?.["email"] as string | undefined; // @cast-boundary db-row
|
|
120
|
+
if (!userRow || !userEmail || userEmail.toLowerCase() !== invitationEmail) {
|
|
120
121
|
return writeFailure(
|
|
121
122
|
new UnprocessableError(AuthErrors.inviteEmailMismatch, {
|
|
122
123
|
i18nKey: "auth.errors.inviteEmailMismatch",
|
|
@@ -131,7 +132,7 @@ export function createInviteAcceptHandler() {
|
|
|
131
132
|
createSystemUser(invitationTenantId),
|
|
132
133
|
"tenant:query:memberships",
|
|
133
134
|
{ userId: event.user.id },
|
|
134
|
-
)) as Array<{ tenantId: string }>;
|
|
135
|
+
)) as Array<{ tenantId: string }>; // @cast-boundary db-row
|
|
135
136
|
const alreadyMember = memberships.some((m) => m.tenantId === invitationTenantId);
|
|
136
137
|
|
|
137
138
|
// @cast-boundary db-runner — TenantDb.raw is DbRunner
|
|
@@ -87,8 +87,8 @@ export function createInviteCreateHandler(opts: InviteCreateOptions = {}) {
|
|
|
87
87
|
let invitationId: string;
|
|
88
88
|
let token: string;
|
|
89
89
|
if (existing) {
|
|
90
|
-
invitationId = existing["id"] as string;
|
|
91
|
-
const existingVersion = existing["version"] as number;
|
|
90
|
+
invitationId = existing["id"] as string; // @cast-boundary db-row
|
|
91
|
+
const existingVersion = existing["version"] as number; // @cast-boundary db-row
|
|
92
92
|
// Resend-Idempotenz: Token aus Redis re-use wenn noch lebend.
|
|
93
93
|
// Sonst neuen mintinen (alter ist abgelaufen).
|
|
94
94
|
const existingToken = await getTokenForInvitation(ctx.redis, invitationId);
|
|
@@ -122,7 +122,7 @@ export function createInviteCreateHandler(opts: InviteCreateOptions = {}) {
|
|
|
122
122
|
ctx.db,
|
|
123
123
|
);
|
|
124
124
|
if (!createResult.isSuccess) return createResult;
|
|
125
|
-
invitationId = (createResult.data as { id: string }).id;
|
|
125
|
+
invitationId = (createResult.data as { id: string }).id; // @cast-boundary engine-payload
|
|
126
126
|
token = generateToken();
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -114,10 +114,10 @@ export function createInviteSignupCompleteHandler() {
|
|
|
114
114
|
if (!invitation || invitation["status"] !== INVITATION_STATUS.pending)
|
|
115
115
|
return invalidInviteToken();
|
|
116
116
|
|
|
117
|
-
const invitationTenantId = invitation["tenantId"] as TenantId;
|
|
118
|
-
const invitationEmail = invitation["email"] as string;
|
|
119
|
-
const invitationRole = invitation["role"] as string;
|
|
120
|
-
const invitationVersion = invitation["version"] as number;
|
|
117
|
+
const invitationTenantId = invitation["tenantId"] as TenantId; // @cast-boundary db-row
|
|
118
|
+
const invitationEmail = invitation["email"] as string; // @cast-boundary db-row
|
|
119
|
+
const invitationRole = invitation["role"] as string; // @cast-boundary db-row
|
|
120
|
+
const invitationVersion = invitation["version"] as number; // @cast-boundary db-row
|
|
121
121
|
|
|
122
122
|
// User-Not-Exists-Check: wenn die Email schon registriert ist,
|
|
123
123
|
// muss der User Branch 2 (acceptWithLogin) nutzen. Hier ist
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
8
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import { UserQueries } from "../../user";
|
|
10
|
+
import { USER_STATUS, UserQueries } from "../../user";
|
|
11
11
|
import { parseAuthUserRow } from "../auth-user-row";
|
|
12
12
|
import {
|
|
13
13
|
AUTH_LOCKOUT_DEFAULT_DURATION_MINUTES,
|
|
@@ -53,6 +53,19 @@ function accountLocked(retryAfterSeconds: number) {
|
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// S2.U6 — DSGVO Art. 18 Account-Freeze. Distinct error code (nicht zu
|
|
57
|
+
// invalid_credentials collapsen) damit UI klar sagen kann "Account ist
|
|
58
|
+
// pausiert, hier klicken zum Aufheben". User weiss schon dass sein Konto
|
|
59
|
+
// restricted ist (er hat selbst die Restriction gesetzt), also kein
|
|
60
|
+
// Enumeration-Leak.
|
|
61
|
+
function accountRestricted() {
|
|
62
|
+
return writeFailure(
|
|
63
|
+
new UnprocessableError(AuthErrors.accountRestricted, {
|
|
64
|
+
i18nKey: "auth.errors.accountRestricted",
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
56
69
|
export type LoginHandlerOptions = {
|
|
57
70
|
// When true, a valid (email + password) login fails with email_not_verified
|
|
58
71
|
// if the user row's emailVerified flag is false. Enumeration-leak is
|
|
@@ -145,12 +158,29 @@ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
|
|
|
145
158
|
return emailNotVerified();
|
|
146
159
|
}
|
|
147
160
|
|
|
161
|
+
// S2.U6 — DSGVO Art. 18 Account-Freeze. Restricted users koennen sich
|
|
162
|
+
// nicht einloggen; lift-restriction-Endpoint ist der einzige Ausgang
|
|
163
|
+
// (siehe lift-restriction.write.ts Header — typisch via Magic-Link
|
|
164
|
+
// oder Operator-Tool, da Login geblockt). Auth-side Block ist hard-
|
|
165
|
+
// requirement; ohne den koennte der User mit Login-Sessions trotz
|
|
166
|
+
// Restriction-Flag durchschreiben.
|
|
167
|
+
//
|
|
168
|
+
// DeletionRequested + Deleted kollabieren bewusst auf invalid_creds
|
|
169
|
+
// (anti-enumeration im Forget-Pfad) — Restricted ist user-initiiert,
|
|
170
|
+
// distinct error ist hier safe.
|
|
171
|
+
if (found.status === USER_STATUS.Restricted) {
|
|
172
|
+
return accountRestricted();
|
|
173
|
+
}
|
|
174
|
+
if (found.status === USER_STATUS.DeletionRequested || found.status === USER_STATUS.Deleted) {
|
|
175
|
+
return invalidCredentials();
|
|
176
|
+
}
|
|
177
|
+
|
|
148
178
|
// Resolve tenant + roles via the tenant feature's memberships query.
|
|
149
179
|
// Returns [] if the user has no memberships — MVP: no login without an
|
|
150
180
|
// invitation, so we refuse with a dedicated error.
|
|
151
181
|
const memberships = (await ctx.queryAs(systemUser, "tenant:query:memberships", {
|
|
152
182
|
userId: found.id,
|
|
153
|
-
})) as Array<{ tenantId: TenantId; roles: readonly string[] }>;
|
|
183
|
+
})) as Array<{ tenantId: TenantId; roles: readonly string[] }>; // @cast-boundary db-runner
|
|
154
184
|
|
|
155
185
|
if (memberships.length === 0) {
|
|
156
186
|
return noMembership();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { access, defineWriteHandler, pipeline } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
// Logout — JWT is stateless, so server-side we only return OK. A future
|
|
@@ -8,5 +8,5 @@ export const logoutWrite = defineWriteHandler({
|
|
|
8
8
|
name: "logout",
|
|
9
9
|
schema: z.object({}),
|
|
10
10
|
access: { roles: access.authenticated },
|
|
11
|
-
|
|
11
|
+
perform: pipeline(({ r }) => [r.step.return({ isSuccess: true, data: { kind: "logged-out" } })]),
|
|
12
12
|
});
|
|
@@ -117,7 +117,7 @@ export function createSignupConfirmHandler() {
|
|
|
117
117
|
},
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
const tenantId = generateId() as TenantId;
|
|
120
|
+
const tenantId = generateId() as TenantId; // @cast-boundary engine-payload
|
|
121
121
|
// Display-Name aus email-prefix als sinnvolles Default; User kann
|
|
122
122
|
// den Tenant-Namen + sein eigenes displayName später ändern.
|
|
123
123
|
const displayName = email.split("@")[0] ?? email;
|
|
@@ -23,6 +23,8 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
23
23
|
"auth.errors.accountLocked": "Konto vorübergehend gesperrt.",
|
|
24
24
|
"auth.errors.accountLockedRetry": "Konto gesperrt. Neuer Versuch in {minutes} Minuten.",
|
|
25
25
|
"auth.errors.emailNotVerified": "E-Mail-Adresse noch nicht bestätigt.",
|
|
26
|
+
"auth.errors.accountRestricted":
|
|
27
|
+
"Konto pausiert (Datenschutz Art. 18). Bitte Pause aufheben um wieder einzuloggen.",
|
|
26
28
|
"auth.errors.rateLimited": "Zu viele Login-Versuche. Bitte kurz warten.",
|
|
27
29
|
"auth.errors.invalidBody": "Ungültige Eingabe.",
|
|
28
30
|
"auth.errors.loginFailed": "Login fehlgeschlagen.",
|
|
@@ -116,6 +118,8 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
116
118
|
"auth.errors.accountLocked": "Account temporarily locked.",
|
|
117
119
|
"auth.errors.accountLockedRetry": "Account locked. Try again in {minutes} minutes.",
|
|
118
120
|
"auth.errors.emailNotVerified": "Email address not yet verified.",
|
|
121
|
+
"auth.errors.accountRestricted":
|
|
122
|
+
"Account paused (GDPR Art. 18). Please lift the restriction to sign in again.",
|
|
119
123
|
"auth.errors.rateLimited": "Too many login attempts. Please wait briefly.",
|
|
120
124
|
"auth.errors.invalidBody": "Invalid input.",
|
|
121
125
|
"auth.errors.loginFailed": "Login failed.",
|
|
@@ -244,7 +244,7 @@ export async function confirmSignup(
|
|
|
244
244
|
body: JSON.stringify({ token, password }),
|
|
245
245
|
});
|
|
246
246
|
if (res.ok) {
|
|
247
|
-
const body = (await res.json()) as SignupConfirmSuccess;
|
|
247
|
+
const body = (await res.json()) as SignupConfirmSuccess; // @cast-boundary engine-payload
|
|
248
248
|
return { ok: true, data: body };
|
|
249
249
|
}
|
|
250
250
|
return { ok: false, error: await parseTokenFailure(res) };
|
|
@@ -18,7 +18,7 @@ import { BILLING_FOUNDATION_FEATURE, SubscriptionStatuses } from "./constants";
|
|
|
18
18
|
export const SUBSCRIPTION_AGGREGATE_TYPE = "subscription" as const;
|
|
19
19
|
|
|
20
20
|
// Event-name-Konstanten — short-form (für r.defineEvent) + qualifizierte
|
|
21
|
-
// FQN (für ctx.
|
|
21
|
+
// FQN (für ctx.unsafeAppendEvent + projection-apply-keys).
|
|
22
22
|
export const SUBSCRIPTION_CREATED_EVENT_SHORT = "subscription-created" as const;
|
|
23
23
|
export const SUBSCRIPTION_UPDATED_EVENT_SHORT = "subscription-updated" as const;
|
|
24
24
|
export const SUBSCRIPTION_CANCELED_EVENT_SHORT = "subscription-canceled" as const;
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
// (z.B. für analytics: "wie viele Wechsel im Monat?"), kommt ein
|
|
42
42
|
// `subscription-provider-changed`-event-type später.
|
|
43
43
|
|
|
44
|
-
import { defineFeature
|
|
44
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
45
45
|
import { BILLING_FOUNDATION_FEATURE, SUBSCRIPTION_PROVIDER_EXTENSION } from "./constants";
|
|
46
46
|
import {
|
|
47
47
|
INVOICE_PAID_EVENT_QN,
|
|
@@ -70,53 +70,50 @@ import {
|
|
|
70
70
|
subscriptionsProjectionTable,
|
|
71
71
|
} from "./projection";
|
|
72
72
|
|
|
73
|
-
export const billingFoundationFeature
|
|
74
|
-
|
|
75
|
-
(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
r.defineEvent(INVOICE_PAID_EVENT_SHORT, subscriptionEventPayloadSchema);
|
|
84
|
-
r.defineEvent(INVOICE_PAYMENT_FAILED_EVENT_SHORT, subscriptionEventPayloadSchema);
|
|
73
|
+
export const billingFoundationFeature = defineFeature(BILLING_FOUNDATION_FEATURE, (r) => {
|
|
74
|
+
// 5 fine-grained domain-events. Alle 5 nutzen denselben payload-
|
|
75
|
+
// shape (= subscription-state-snapshot); der event-type taggt was
|
|
76
|
+
// passiert ist. Future-consumer (billing-history, accounting)
|
|
77
|
+
// listenen direkt auf den event-type ohne payload-discriminator.
|
|
78
|
+
r.defineEvent(SUBSCRIPTION_CREATED_EVENT_SHORT, subscriptionEventPayloadSchema);
|
|
79
|
+
r.defineEvent(SUBSCRIPTION_UPDATED_EVENT_SHORT, subscriptionEventPayloadSchema);
|
|
80
|
+
r.defineEvent(SUBSCRIPTION_CANCELED_EVENT_SHORT, subscriptionEventPayloadSchema);
|
|
81
|
+
r.defineEvent(INVOICE_PAID_EVENT_SHORT, subscriptionEventPayloadSchema);
|
|
82
|
+
r.defineEvent(INVOICE_PAYMENT_FAILED_EVENT_SHORT, subscriptionEventPayloadSchema);
|
|
85
83
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
84
|
+
// Inline projection: materialized current state in `read_subscriptions`.
|
|
85
|
+
// Apply läuft in derselben TX wie ctx.unsafeAppendEvent — read-your-
|
|
86
|
+
// own-write ohne dispatcher-tick.
|
|
87
|
+
r.projection({
|
|
88
|
+
name: "subscription",
|
|
89
|
+
source: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
90
|
+
table: subscriptionsProjectionTable,
|
|
91
|
+
apply: {
|
|
92
|
+
[SUBSCRIPTION_CREATED_EVENT_QN]: applySubscriptionCreated,
|
|
93
|
+
[SUBSCRIPTION_UPDATED_EVENT_QN]: applySubscriptionUpdated,
|
|
94
|
+
[SUBSCRIPTION_CANCELED_EVENT_QN]: applySubscriptionCanceled,
|
|
95
|
+
[INVOICE_PAID_EVENT_QN]: applyInvoicePaid,
|
|
96
|
+
[INVOICE_PAYMENT_FAILED_EVENT_QN]: applyInvoicePaymentFailed,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
// Plugin extension-point. Provider-Plugins registrieren sich hier.
|
|
101
|
+
r.extendsRegistrar(SUBSCRIPTION_PROVIDER_EXTENSION, {
|
|
102
|
+
onRegister: () => {
|
|
103
|
+
// No side-effects at register-time.
|
|
104
|
+
},
|
|
105
|
+
});
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
// Custom write-handlers:
|
|
108
|
+
// - process-event: programmatic entry-point vom webhook-handler;
|
|
109
|
+
// dispatcht zu type-passendem appendEvent
|
|
110
|
+
// - create-checkout-session: Tenant-Admin "Upgrade to Pro"-flow
|
|
111
|
+
// - create-portal-session: Tenant-Admin "Manage Subscription"-flow
|
|
112
|
+
r.writeHandler(processEventHandler);
|
|
113
|
+
r.writeHandler(createCheckoutSessionHandler);
|
|
114
|
+
r.writeHandler(createPortalSessionHandler);
|
|
117
115
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
);
|
|
116
|
+
// Custom list-query auf der subscription-projection (raw drizzle-
|
|
117
|
+
// table; kein r.entity weil Schreiben via projection-apply läuft).
|
|
118
|
+
r.queryHandler(listSubscriptionsQuery);
|
|
119
|
+
});
|
|
@@ -28,7 +28,7 @@ export const createPortalSessionHandler: WriteHandlerDef = {
|
|
|
28
28
|
schema: createPortalSessionSchema,
|
|
29
29
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
30
30
|
handler: async (event, ctx) => {
|
|
31
|
-
const payload = event.payload as CreatePortalSessionPayload;
|
|
31
|
+
const payload = event.payload as CreatePortalSessionPayload; // @cast-boundary engine-payload
|
|
32
32
|
const tenantId = event.user.tenantId;
|
|
33
33
|
|
|
34
34
|
// 1. Hol current subscription-row für den Tenant. Aggregate-id ist
|
|
@@ -41,8 +41,8 @@ export const createPortalSessionHandler: WriteHandlerDef = {
|
|
|
41
41
|
"subscription-foundation: no active subscription for this tenant. Create one via create-checkout-session first.",
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
|
-
const providerName = row["providerName"] as string;
|
|
45
|
-
const providerCustomerId = row["providerCustomerId"] as string;
|
|
44
|
+
const providerName = row["providerName"] as string; // @cast-boundary db-row
|
|
45
|
+
const providerCustomerId = row["providerCustomerId"] as string; // @cast-boundary db-row
|
|
46
46
|
|
|
47
47
|
// 2. Plugin-Lookup
|
|
48
48
|
const usages = ctx.registry.getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// kein zweiter append.
|
|
10
10
|
// 2. Type-mapping: SubscriptionEvent.type (= normalisiert vom Plugin)
|
|
11
11
|
// → einer der 5 ES-event-typen.
|
|
12
|
-
// 3. ctx.
|
|
12
|
+
// 3. ctx.unsafeAppendEvent — Inline-projection materialisiert die
|
|
13
13
|
// `read_subscriptions`-row in derselben TX.
|
|
14
14
|
|
|
15
15
|
import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -69,7 +69,7 @@ const NORMALIZED_TO_ES_EVENT: Readonly<Record<string, string>> = {
|
|
|
69
69
|
[SubscriptionEventTypes.canceled]: SUBSCRIPTION_CANCELED_EVENT_QN,
|
|
70
70
|
[SubscriptionEventTypes.invoicePaid]: INVOICE_PAID_EVENT_QN,
|
|
71
71
|
[SubscriptionEventTypes.invoicePaymentFailed]: INVOICE_PAYMENT_FAILED_EVENT_QN,
|
|
72
|
-
}
|
|
72
|
+
} satisfies Readonly<Record<string, string>>;
|
|
73
73
|
|
|
74
74
|
// =============================================================================
|
|
75
75
|
// Handler
|
|
@@ -141,7 +141,7 @@ export const processEventHandler: WriteHandlerDef = {
|
|
|
141
141
|
providerName: payload.providerName,
|
|
142
142
|
rawPayload: payload.rawPayload,
|
|
143
143
|
};
|
|
144
|
-
await ctx.
|
|
144
|
+
await ctx.unsafeAppendEvent({
|
|
145
145
|
aggregateId: aggId,
|
|
146
146
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
147
147
|
type: esEventType,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Inline-projection für `read_subscriptions`. Materialisiert die 5
|
|
2
2
|
// subscription-events in eine row pro Tenant.
|
|
3
3
|
//
|
|
4
|
-
// Apply läuft in derselben TX wie ctx.
|
|
4
|
+
// Apply läuft in derselben TX wie ctx.unsafeAppendEvent — Caller sieht
|
|
5
5
|
// seinen Schreib-State sofort (kein dispatcher-tick nötig). PK = event.
|
|
6
6
|
// aggregateId (= deterministic uuidv5 pro Tenant) → replays kollidieren
|
|
7
7
|
// auf der PK statt doppelte rows zu erzeugen.
|
|
@@ -169,7 +169,7 @@ export function createSubscriptionWebhookHandler(deps: SubscriptionWebhookDeps)
|
|
|
169
169
|
);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
return c.json({ processed: true, ...((dispatched.data as object) ?? {}) }, 200);
|
|
172
|
+
return c.json({ processed: true, ...((dispatched.data as object) ?? {}) }, 200); // @cast-boundary engine-bridge
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
|