@cosmicdrift/kumiko-bundled-features 0.2.3 → 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 +60 -0
- package/package.json +17 -14
- 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 +1 -1
- 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/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/handlers/for-tenant.query.ts +7 -6
- package/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
- package/src/compliance-profiles/handlers/set-profile.write.ts +6 -8
- package/src/compliance-profiles/resolve-for-tenant.ts +7 -5
- package/src/compliance-profiles/seeding.ts +1 -1
- package/src/config/resolver.ts +1 -1
- package/src/data-retention/_internal/parse-override.ts +3 -2
- package/src/data-retention/handlers/policy-for.query.ts +1 -1
- package/src/data-retention/keep-for.ts +1 -1
- package/src/data-retention/presets.ts +1 -1
- package/src/data-retention/resolve-for-tenant.ts +1 -1
- 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 +1 -1
- package/src/file-provider-s3/feature.ts +2 -2
- package/src/files-provider-s3/s3-provider.ts +2 -2
- 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/handlers/rotate.job.ts +2 -2
- package/src/sessions/handlers/cleanup.job.ts +2 -2
- 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/feature.ts +8 -8
- package/src/user/handlers/find-for-auth.query.ts +1 -1
- package/src/user/seeding.ts +2 -2
- package/src/user-data-rights/feature.ts +4 -3
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
- package/src/user-data-rights/handlers/download-by-job.query.ts +8 -11
- package/src/user-data-rights/handlers/download-by-token.query.ts +14 -16
- package/src/user-data-rights/handlers/export-status.query.ts +1 -1
- package/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
- package/src/user-data-rights/handlers/request-export.write.ts +2 -2
- package/src/user-data-rights/run-export-jobs.ts +2 -2
- package/src/user-data-rights/run-forget-cleanup.ts +27 -28
- package/src/user-data-rights/run-user-export.ts +1 -1
- package/src/user-data-rights/token-helpers.ts +2 -2
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +1 -1
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
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
|
+
|
|
3
63
|
## 0.2.3
|
|
4
64
|
|
|
5
65
|
### 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>",
|
|
@@ -63,26 +63,29 @@
|
|
|
63
63
|
"./feature-toggles": "./src/feature-toggles/index.ts",
|
|
64
64
|
"./text-content": "./src/text-content/index.ts",
|
|
65
65
|
"./text-content/seeding": "./src/text-content/seeding.ts",
|
|
66
|
-
"./
|
|
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"
|
|
67
70
|
},
|
|
68
71
|
"dependencies": {
|
|
69
|
-
"@aws-sdk/client-s3": "^3.
|
|
70
|
-
"@aws-sdk/lib-storage": "^3.
|
|
71
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
72
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
73
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
74
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
75
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
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",
|
|
76
79
|
"@mollie/api-client": "^4.5.0",
|
|
77
80
|
"@node-rs/argon2": "^2.0.2",
|
|
78
81
|
"@types/nodemailer": "^8.0.0",
|
|
79
82
|
"clsx": "^2.1.1",
|
|
80
|
-
"lucide-react": "^1.
|
|
81
|
-
"marked": "^
|
|
83
|
+
"lucide-react": "^1.14.0",
|
|
84
|
+
"marked": "^18.0.3",
|
|
82
85
|
"nodemailer": "^8.0.7",
|
|
83
|
-
"react": "^19.2.
|
|
84
|
-
"stripe": "^22.1.
|
|
85
|
-
"tailwind-merge": "^3.0
|
|
86
|
+
"react": "^19.2.6",
|
|
87
|
+
"stripe": "^22.1.1",
|
|
88
|
+
"tailwind-merge": "^3.6.0"
|
|
86
89
|
},
|
|
87
90
|
"publishConfig": {
|
|
88
91
|
"registry": "https://registry.npmjs.org",
|
|
@@ -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
|
|
@@ -180,7 +180,7 @@ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
|
|
|
180
180
|
// invitation, so we refuse with a dedicated error.
|
|
181
181
|
const memberships = (await ctx.queryAs(systemUser, "tenant:query:memberships", {
|
|
182
182
|
userId: found.id,
|
|
183
|
-
})) as Array<{ tenantId: TenantId; roles: readonly string[] }>;
|
|
183
|
+
})) as Array<{ tenantId: TenantId; roles: readonly string[] }>; // @cast-boundary db-runner
|
|
184
184
|
|
|
185
185
|
if (memberships.length === 0) {
|
|
186
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;
|
|
@@ -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
|
|
|
@@ -12,7 +12,7 @@ export const CAP_COUNTER_ROLLING_AGGREGATE_TYPE = "cap-counter-rolling" as const
|
|
|
12
12
|
// Custom event-type für Rolling-Window-Counter. Symmetrisches Paar:
|
|
13
13
|
// _SHORT — passt zu `r.defineEvent(short, schema)` im Registrar
|
|
14
14
|
// (Framework prefixt automatisch zu QN)
|
|
15
|
-
// _QN — qualifizierte Form für `ctx.
|
|
15
|
+
// _QN — qualifizierte Form für `ctx.unsafeAppendEvent({type})`
|
|
16
16
|
// + `events.type`-Spalte + `registry.getEvent(qn)`-Lookup
|
|
17
17
|
// Beide MÜSSEN konsistent sein (drift-pin im feature-test).
|
|
18
18
|
export const ROLLING_INCREMENTED_EVENT_SHORT = "rolling-incremented" as const;
|
|
@@ -118,7 +118,7 @@ export async function enforceCap(
|
|
|
118
118
|
.limit(1);
|
|
119
119
|
|
|
120
120
|
const row = rows[0];
|
|
121
|
-
const value = row ? (row["value"] as number) : 0;
|
|
121
|
+
const value = row ? (row["value"] as number) : 0; // @cast-boundary db-row
|
|
122
122
|
|
|
123
123
|
if (value >= hardThreshold) {
|
|
124
124
|
throw new CapExceededError(options.capName, options.limit, value, tolerance);
|
|
@@ -46,11 +46,7 @@
|
|
|
46
46
|
// config, kein secrets, kein tenant-feature nötig. Tenant-Scoping kommt
|
|
47
47
|
// vom Framework-Default (Base-Column tenantId).
|
|
48
48
|
|
|
49
|
-
import {
|
|
50
|
-
defineEntityListHandler,
|
|
51
|
-
defineFeature,
|
|
52
|
-
type FeatureDefinition,
|
|
53
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
49
|
+
import { defineEntityListHandler, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
54
50
|
import { CAP_COUNTER_FEATURE, ROLLING_INCREMENTED_EVENT_SHORT } from "./constants";
|
|
55
51
|
import { capCounterEntity } from "./entity";
|
|
56
52
|
import { getCounterQuery } from "./handlers/get-counter.query";
|
|
@@ -63,11 +59,11 @@ import { markSoftWarnedHandler } from "./handlers/mark-soft-warned.write";
|
|
|
63
59
|
|
|
64
60
|
const sysadminAccess = { access: { roles: ["SystemAdmin"] } } as const;
|
|
65
61
|
|
|
66
|
-
export const capCounterFeature
|
|
62
|
+
export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
|
|
67
63
|
r.entity("cap-counter", capCounterEntity);
|
|
68
64
|
|
|
69
65
|
// Custom Domain-Event für Rolling-Counter. r.defineEvent registriert
|
|
70
|
-
// das Schema beim Registry; ctx.
|
|
66
|
+
// das Schema beim Registry; ctx.unsafeAppendEvent im Handler nutzt
|
|
71
67
|
// dasselbe Schema für Append-Time-Validation. QN nach Prefixing:
|
|
72
68
|
// "cap-counter:event:rolling-incremented" (siehe
|
|
73
69
|
// ROLLING_INCREMENTED_EVENT_QN).
|
|
@@ -22,7 +22,7 @@ export const getCounterQuery: QueryHandlerDef = {
|
|
|
22
22
|
schema: getCounterSchema,
|
|
23
23
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
24
24
|
handler: async (query, ctx) => {
|
|
25
|
-
const { capName, periodStartIso } = query.payload as z.infer<typeof getCounterSchema>;
|
|
25
|
+
const { capName, periodStartIso } = query.payload as z.infer<typeof getCounterSchema>; // @cast-boundary engine-payload
|
|
26
26
|
|
|
27
27
|
// ctx.db is tenant-scoped; filter by capName + periodStart explicitly.
|
|
28
28
|
const rows = await ctx.db
|
|
@@ -60,11 +60,11 @@ export const incrementRollingCapHandler: WriteHandlerDef = {
|
|
|
60
60
|
const payload = event.payload as IncrementRollingPayload;
|
|
61
61
|
const aggregateId = rollingCapAggregateId(event.user.tenantId, payload.capName);
|
|
62
62
|
|
|
63
|
-
//
|
|
63
|
+
// unsafeAppendEvent — bundled-features-Pfad (apps mit yarn kumiko
|
|
64
64
|
// codegen kriegen den strict-typed appendEvent-Wrapper). Schema-
|
|
65
65
|
// Validation läuft trotzdem, weil r.defineEvent das Schema
|
|
66
66
|
// registriert hat.
|
|
67
|
-
await ctx.
|
|
67
|
+
await ctx.unsafeAppendEvent({
|
|
68
68
|
aggregateId,
|
|
69
69
|
aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
70
70
|
type: ROLLING_INCREMENTED_EVENT_QN,
|
|
@@ -46,7 +46,7 @@ export const incrementCapHandler: WriteHandlerDef = {
|
|
|
46
46
|
// which subsystem incremented.
|
|
47
47
|
access: { roles: ["SystemAdmin"] },
|
|
48
48
|
handler: async (event, ctx) => {
|
|
49
|
-
const payload = event.payload as IncrementPayload;
|
|
49
|
+
const payload = event.payload as IncrementPayload; // @cast-boundary engine-payload
|
|
50
50
|
const aggregateId = capCounterAggregateId(
|
|
51
51
|
event.user.tenantId,
|
|
52
52
|
payload.capName,
|
|
@@ -77,8 +77,8 @@ export const incrementCapHandler: WriteHandlerDef = {
|
|
|
77
77
|
// clearer than a possibly-null deref later.
|
|
78
78
|
throw new Error("cap-counter:increment: row vanished between length-check and read");
|
|
79
79
|
}
|
|
80
|
-
const currentValue = currentRow["value"] as number;
|
|
81
|
-
const currentVersion = currentRow["version"] as number;
|
|
80
|
+
const currentValue = currentRow["value"] as number; // @cast-boundary db-row
|
|
81
|
+
const currentVersion = currentRow["version"] as number; // @cast-boundary db-row
|
|
82
82
|
return executor.update(
|
|
83
83
|
{
|
|
84
84
|
id: aggregateId,
|
|
@@ -25,7 +25,7 @@ export const markSoftWarnedHandler: WriteHandlerDef = {
|
|
|
25
25
|
schema: markSoftWarnedSchema,
|
|
26
26
|
access: { roles: ["SystemAdmin"] },
|
|
27
27
|
handler: async (event, ctx) => {
|
|
28
|
-
const payload = event.payload as z.infer<typeof markSoftWarnedSchema>;
|
|
28
|
+
const payload = event.payload as z.infer<typeof markSoftWarnedSchema>; // @cast-boundary engine-payload
|
|
29
29
|
const aggregateId = capCounterAggregateId(
|
|
30
30
|
event.user.tenantId,
|
|
31
31
|
payload.capName,
|
|
@@ -42,7 +42,7 @@ export const markSoftWarnedHandler: WriteHandlerDef = {
|
|
|
42
42
|
if (!row) {
|
|
43
43
|
throw new Error("cap-counter:mark-soft-warned: row vanished between length-check and read");
|
|
44
44
|
}
|
|
45
|
-
const currentVersion = row["version"] as number;
|
|
45
|
+
const currentVersion = row["version"] as number; // @cast-boundary db-row
|
|
46
46
|
|
|
47
47
|
return executor.update(
|
|
48
48
|
{
|
|
@@ -34,7 +34,7 @@ export function createEmailChannel(options: EmailChannelOptions): DeliveryChanne
|
|
|
34
34
|
template: message.notificationType,
|
|
35
35
|
variables,
|
|
36
36
|
});
|
|
37
|
-
const subject = (variables["subject"] as string) ?? message.title;
|
|
37
|
+
const subject = (variables["subject"] as string) ?? message.title; // @cast-boundary dynamic-key
|
|
38
38
|
|
|
39
39
|
await transport.send({
|
|
40
40
|
to: address,
|
|
@@ -20,7 +20,7 @@ export function createInMemoryTransport(): EmailTransport & {
|
|
|
20
20
|
const sent: EmailMessage[] = [];
|
|
21
21
|
const transport = {
|
|
22
22
|
sent,
|
|
23
|
-
failNext: null as null | { message: string },
|
|
23
|
+
failNext: null as null | { message: string }, // @cast-boundary generic-record
|
|
24
24
|
async send(message: EmailMessage) {
|
|
25
25
|
if (transport.failNext) {
|
|
26
26
|
const err = new Error(transport.failNext.message);
|