@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
|
@@ -17,8 +17,11 @@
|
|
|
17
17
|
// **NICHT für Production.** Buffer ist Process-Memory, geht beim
|
|
18
18
|
// Restart verloren + wächst monoton mit jedem write.
|
|
19
19
|
|
|
20
|
-
import type {
|
|
21
|
-
|
|
20
|
+
import type {
|
|
21
|
+
FileProviderContext,
|
|
22
|
+
FileProviderPlugin,
|
|
23
|
+
} from "@cosmicdrift/kumiko-bundled-features/file-foundation";
|
|
24
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
22
25
|
import {
|
|
23
26
|
createInMemoryFileProvider,
|
|
24
27
|
type FileStorageProvider,
|
|
@@ -63,7 +66,7 @@ export const fileProviderInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
63
66
|
r.requires("file-foundation");
|
|
64
67
|
|
|
65
68
|
const plugin: FileProviderPlugin = {
|
|
66
|
-
build: async (_ctx:
|
|
69
|
+
build: async (_ctx: FileProviderContext, tenantId: string): Promise<FileStorageProvider> => {
|
|
67
70
|
// Returnt den per-tenant Storage. Identitätsstabil zwischen calls
|
|
68
71
|
// damit accumulated state erhalten bleibt.
|
|
69
72
|
return getOrCreateProviderForTenant(tenantId);
|
|
@@ -18,19 +18,17 @@
|
|
|
18
18
|
//
|
|
19
19
|
// **Boot-Dependencies:** config + secrets + file-foundation.
|
|
20
20
|
|
|
21
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
FileProviderContext,
|
|
23
|
+
FileProviderPlugin,
|
|
24
|
+
} from "@cosmicdrift/kumiko-bundled-features/file-foundation";
|
|
22
25
|
import { createS3Provider } from "@cosmicdrift/kumiko-bundled-features/files-provider-s3";
|
|
23
26
|
import {
|
|
24
27
|
requireDefined,
|
|
25
28
|
requireNonEmpty,
|
|
26
29
|
} from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
|
|
27
30
|
import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
28
|
-
import {
|
|
29
|
-
access,
|
|
30
|
-
createTenantConfig,
|
|
31
|
-
defineFeature,
|
|
32
|
-
type HandlerContext,
|
|
33
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
31
|
+
import { access, createTenantConfig, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
34
32
|
import type { FileStorageProvider } from "@cosmicdrift/kumiko-framework/files";
|
|
35
33
|
|
|
36
34
|
const FEATURE_NAME = "file-provider-s3";
|
|
@@ -89,7 +87,7 @@ export const fileProviderS3Feature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
89
87
|
// Plugin-Registration. entityName "s3" ist was tenants in
|
|
90
88
|
// file-foundation's `provider` config-key setzen.
|
|
91
89
|
const plugin: FileProviderPlugin = {
|
|
92
|
-
build: async (ctx:
|
|
90
|
+
build: async (ctx: FileProviderContext, tenantId: string) => buildS3Provider(ctx, tenantId),
|
|
93
91
|
};
|
|
94
92
|
r.useExtension("fileProvider", "s3", plugin);
|
|
95
93
|
|
|
@@ -104,7 +102,7 @@ export const S3_SECRET_ACCESS_KEY = fileProviderS3Feature.exports.secretAccessKe
|
|
|
104
102
|
// =============================================================================
|
|
105
103
|
|
|
106
104
|
async function buildS3Provider(
|
|
107
|
-
ctx:
|
|
105
|
+
ctx: FileProviderContext,
|
|
108
106
|
tenantId: string,
|
|
109
107
|
): Promise<FileStorageProvider> {
|
|
110
108
|
const ctxConfig = ctx.config;
|
|
@@ -131,13 +129,13 @@ async function buildS3Provider(
|
|
|
131
129
|
await ctxConfig(fileProviderS3Feature.exports.configKeys.endpoint),
|
|
132
130
|
FEATURE_NAME,
|
|
133
131
|
"endpoint",
|
|
134
|
-
) as string;
|
|
132
|
+
) as string; // @cast-boundary engine-payload
|
|
135
133
|
const endpoint = endpointRaw.length > 0 ? endpointRaw : undefined;
|
|
136
134
|
const forcePathStyle = requireDefined(
|
|
137
135
|
await ctxConfig(fileProviderS3Feature.exports.configKeys.forcePathStyle),
|
|
138
136
|
FEATURE_NAME,
|
|
139
137
|
"forcePathStyle",
|
|
140
|
-
) as boolean;
|
|
138
|
+
) as boolean; // @cast-boundary engine-payload
|
|
141
139
|
const accessKeyId = requireNonEmpty(
|
|
142
140
|
await ctxConfig(fileProviderS3Feature.exports.configKeys.accessKeyId),
|
|
143
141
|
FEATURE_NAME,
|
|
@@ -157,7 +155,7 @@ async function buildS3Provider(
|
|
|
157
155
|
});
|
|
158
156
|
}
|
|
159
157
|
|
|
160
|
-
async function readSecretAccessKey(ctx:
|
|
158
|
+
async function readSecretAccessKey(ctx: FileProviderContext, tenantId: string): Promise<string> {
|
|
161
159
|
const secrets = requireSecretsContext(ctx, FEATURE_NAME);
|
|
162
160
|
const branded = await secrets.get(tenantId, S3_SECRET_ACCESS_KEY);
|
|
163
161
|
if (!branded) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# files
|
|
2
|
+
|
|
3
|
+
Schema-Sicht der framework-internen `file_refs`-Tabelle als bundled-feature.
|
|
4
|
+
Sprint-1.5 Refactor (Pre-Sprint-2).
|
|
5
|
+
|
|
6
|
+
## Was es macht
|
|
7
|
+
|
|
8
|
+
Deklariert `r.entity("fileRef", ...)` für die DB-Tabelle die das
|
|
9
|
+
Framework via `createFileRoutes` (multipart-Upload + binary-Download)
|
|
10
|
+
bewirtschaftet. Das öffnet die Tür für Cross-Feature-Hooks:
|
|
11
|
+
|
|
12
|
+
- **Sprint 2** (`user-data-rights-defaults`) registriert `r.useExtension(EXT_USER_DATA, "fileRef", { export, delete })` — Forget-Flow + Daten-Export fassen die Files automatisch an. ✅ done
|
|
13
|
+
- **Sprint 5** (`tenant-lifecycle`) wird `r.useExtension(EXT_TENANT_DATA, "fileRef", { destroy })` registrieren — Tenant-Destroy löscht alle FileRefs.
|
|
14
|
+
|
|
15
|
+
## Was es NICHT macht
|
|
16
|
+
|
|
17
|
+
- **Keine Upload-/Download-Routes** — die bleiben in
|
|
18
|
+
`framework/src/api/server.ts` via `options.files`-Bootstrap.
|
|
19
|
+
Multipart-Form-Body und Binary-Streaming passen nicht ins Write/Query-
|
|
20
|
+
Handler-Pattern; ein Refactor zu `r.httpRoute` wäre orthogonal zu
|
|
21
|
+
diesem Sprint.
|
|
22
|
+
- **Kein eigener Drizzle-Table-Build** — die `file_refs`-Tabelle
|
|
23
|
+
existiert schon in `framework/src/files/file-ref-table.ts`. Diese
|
|
24
|
+
Entity ist nur die Schema-Sicht für Cross-Feature-Hooks; Drizzle-
|
|
25
|
+
Queries laufen weiter über `fileRefsTable` aus
|
|
26
|
+
`@cosmicdrift/kumiko-framework/files`.
|
|
27
|
+
|
|
28
|
+
## PII-Annotations (Sprint 0.1+0.7)
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
fileName → pii: true (Originalname enthält oft Personen-Bezug)
|
|
32
|
+
storageKey → allowPlaintext: "is-business-data" (interner UUID-Key)
|
|
33
|
+
mimeType → allowPlaintext: "is-business-data"
|
|
34
|
+
size → allowPlaintext: "is-business-data"
|
|
35
|
+
entityType → allowPlaintext: "is-business-data"
|
|
36
|
+
entityId → allowPlaintext: "is-business-data"
|
|
37
|
+
fieldName → allowPlaintext: "is-business-data"
|
|
38
|
+
insertedAt → kein PII-Marker (Audit-Timestamp, Framework-managed)
|
|
39
|
+
insertedById → allowPlaintext: "is-business-data" (User-Reference, kein Eigen-PII)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`fileName: pii: true` heißt: Sprint 3 Crypto-Shredding wird den Wert
|
|
43
|
+
mit dem Author-Subject-Key encrypten (für File-INHALTE: separates
|
|
44
|
+
Subject-Resolver-Pattern via `subjectField` — siehe storage-encryption.md
|
|
45
|
+
Sprint 4).
|
|
46
|
+
|
|
47
|
+
## Tests
|
|
48
|
+
|
|
49
|
+
`__tests__/files.integration.ts` — 5 Tests die beweisen dass die Feature-
|
|
50
|
+
Definition clean lädt + die PII-Markers + Tabellenname stimmen.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// files-feature integration tests (S1.5 + S1.7).
|
|
2
|
+
//
|
|
3
|
+
// Tests sortiert nach Verhaltens-Tiefe:
|
|
4
|
+
// 1. Feature-Definition Smoke (Boot-Validation passes)
|
|
5
|
+
// 2. Cross-Feature-Behavior: fileRef-Entity ist als Hook-Anker für
|
|
6
|
+
// Sprint-2-userData-Extension nutzbar
|
|
7
|
+
// 3. DDL-Konsistenz: Framework-pgTable + Feature-Entity zeigen auf
|
|
8
|
+
// dieselbe Postgres-Struktur (Drift-Guard)
|
|
9
|
+
// 4. Event-QN-Match: r.defineEvent + framework's fileUploadedEvent
|
|
10
|
+
// resolven zum selben QN
|
|
11
|
+
|
|
12
|
+
import { defineFeature, EXT_USER_DATA } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
|
+
import { FILE_UPLOADED_EVENT_TYPE, fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
|
|
14
|
+
import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
15
|
+
import { getTableColumns } from "drizzle-orm";
|
|
16
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
17
|
+
import { createFilesFeature, fileRefEntity } from "../feature";
|
|
18
|
+
|
|
19
|
+
let stack: TestStack;
|
|
20
|
+
|
|
21
|
+
const feature = createFilesFeature();
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
stack = await setupTestStack({ features: [feature] });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll(async () => {
|
|
28
|
+
await stack.cleanup();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("files :: feature-definition smoke", () => {
|
|
32
|
+
test("Boot ist clean (PII-Annotations + Event-Schema valide)", async () => {
|
|
33
|
+
// setupTestStack hat das feature im beforeAll geladen — wenn Boot-
|
|
34
|
+
// Validation fehlschlägt, würde dieser Block nie laufen.
|
|
35
|
+
expect(stack).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("Tabellen-Name matched die Framework-pgTable (file_refs)", () => {
|
|
39
|
+
expect(fileRefEntity.table).toBe("file_refs");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("fileName ist als pii: true markiert", () => {
|
|
43
|
+
const fileName = fileRefEntity.fields["fileName"] as { pii?: boolean };
|
|
44
|
+
expect(fileName.pii).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("files :: cross-feature behavior (F1, S1.7)", () => {
|
|
49
|
+
test("Sprint-2-Pattern: ein Consumer-Feature kann r.useExtension(EXT_USER_DATA, fileRef, ...) registrieren", async () => {
|
|
50
|
+
// Stub-Feature simuliert die Sprint-2-Semantik: extendsRegistrar
|
|
51
|
+
// (kommt aus user-data-rights) + useExtension auf fileRef.
|
|
52
|
+
const userDataProvider = defineFeature("test-user-data-provider", (r) => {
|
|
53
|
+
r.extendsRegistrar(EXT_USER_DATA, {
|
|
54
|
+
// Spec-stub — Sprint 2 wird die echten Hook-Signaturen liefern.
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const consumer = defineFeature("test-files-consumer", (r) => {
|
|
59
|
+
r.requires("files", "test-user-data-provider");
|
|
60
|
+
r.useExtension(EXT_USER_DATA, "fileRef", {
|
|
61
|
+
// Stub-Hooks: in Sprint 2 werden diese die echte Forget-/Export-
|
|
62
|
+
// Logik tragen. Hier reicht: useExtension findet die fileRef-
|
|
63
|
+
// Entity in der Registry → kein Boot-Error.
|
|
64
|
+
export: async () => [],
|
|
65
|
+
delete: async () => undefined,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Eigener Test-Stack damit dieser Sub-Test isoliert vom outer
|
|
70
|
+
// beforeAll-Stack laeuft.
|
|
71
|
+
const crossStack = await setupTestStack({
|
|
72
|
+
features: [feature, userDataProvider, consumer],
|
|
73
|
+
});
|
|
74
|
+
expect(crossStack).toBeDefined();
|
|
75
|
+
await crossStack.cleanup();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("useExtension auf nicht-existierendes Entity wirft beim Boot", async () => {
|
|
79
|
+
const userDataProvider = defineFeature("test-user-data-provider-2", (r) => {
|
|
80
|
+
r.extendsRegistrar(EXT_USER_DATA, {});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const consumer = defineFeature("test-broken-consumer", (r) => {
|
|
84
|
+
r.requires("test-user-data-provider-2");
|
|
85
|
+
// GHOSTLY: useExtension auf "ghostEntity" gibt es nirgends.
|
|
86
|
+
r.useExtension(EXT_USER_DATA, "ghostEntity", {});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Boot soll laufen — es ist NICHT der Job des Boot-Validators zu
|
|
90
|
+
// pruefen ob die referenced Entity existiert (Extensions sind
|
|
91
|
+
// generisch, "ghostEntity" könnte ein App-Entity in einem anderen
|
|
92
|
+
// Feature sein). Test dokumentiert das aktuelle Verhalten als
|
|
93
|
+
// Regression-Guard. Sprint 2 schaerft ggf. mit
|
|
94
|
+
// validateUserDataExtensionTargets.
|
|
95
|
+
const ghostStack = await setupTestStack({
|
|
96
|
+
features: [userDataProvider, consumer],
|
|
97
|
+
});
|
|
98
|
+
expect(ghostStack).toBeDefined();
|
|
99
|
+
await ghostStack.cleanup();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("files :: DDL-Konsistenz (M3, S1.7)", () => {
|
|
104
|
+
// Drizzle's getTableColumns liefert die typed column-map ohne den
|
|
105
|
+
// Symbol-Properties-Junk. Sauberer als Object.keys(table) das auch
|
|
106
|
+
// interne Drizzle-Symbols mitnimmt.
|
|
107
|
+
function pgColumnNames(): Set<string> {
|
|
108
|
+
return new Set(Object.keys(getTableColumns(fileRefsTable)));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
test("Feature-Entity-Felder matchen die Framework-pgTable column-set", () => {
|
|
112
|
+
// Framework's fileRefsTable ist die Quelle der Wahrheit fuer die
|
|
113
|
+
// DB-Struktur. Feature-Entity ist Schema-Sicht. Beide muessen
|
|
114
|
+
// konsistent sein — sonst landet Sprint-2's userData-Hook in
|
|
115
|
+
// Drift-Hell beim Forget-Flow.
|
|
116
|
+
//
|
|
117
|
+
// Vergleich: alle Feature-Felder muessen als Spalten in der pgTable
|
|
118
|
+
// existieren (umgekehrt darf pgTable framework-managed Spalten haben
|
|
119
|
+
// wie tenantId/createdAt/updatedAt/deletedAt — die deklariert das
|
|
120
|
+
// Framework automatisch beim buildDrizzleTable-Mapping).
|
|
121
|
+
const pgColumns = pgColumnNames();
|
|
122
|
+
const featureFields = Object.keys(fileRefEntity.fields);
|
|
123
|
+
|
|
124
|
+
for (const field of featureFields) {
|
|
125
|
+
expect(
|
|
126
|
+
pgColumns.has(field),
|
|
127
|
+
`Feature-Field "${field}" fehlt in framework pgTable file_refs — Schema-Drift!`,
|
|
128
|
+
).toBe(true);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("Framework-pgTable hat die kritischen file_ref-Spalten (storageKey, fileName, mimeType, size)", () => {
|
|
133
|
+
const pgColumns = pgColumnNames();
|
|
134
|
+
expect(pgColumns.has("storageKey")).toBe(true);
|
|
135
|
+
expect(pgColumns.has("fileName")).toBe(true);
|
|
136
|
+
expect(pgColumns.has("mimeType")).toBe(true);
|
|
137
|
+
expect(pgColumns.has("size")).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("files :: event-QN-match (M4, S1.7)", () => {
|
|
142
|
+
test("framework's fileUploadedEvent.name === 'files:event:uploaded'", () => {
|
|
143
|
+
// Wenn das Framework den Event-Namen aendert, fliegt dieser Test
|
|
144
|
+
// sofort an — und der QN aus r.defineEvent("uploaded") im feature
|
|
145
|
+
// wuerde nicht mehr matchen. Drift-Guard.
|
|
146
|
+
expect(FILE_UPLOADED_EVENT_TYPE).toBe("files:event:uploaded");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("Feature-Name 'files' + Event-Short 'uploaded' = QN 'files:event:uploaded'", () => {
|
|
150
|
+
// r.defineEvent("uploaded") in defineFeature("files", ...) resolved
|
|
151
|
+
// zu QN "files:event:uploaded" via Framework-Convention. Match
|
|
152
|
+
// garantiert dass framework's appendEvent + EventDef-Schema-
|
|
153
|
+
// Validation auf demselben QN landen.
|
|
154
|
+
const expected = `${feature.name}:event:uploaded`;
|
|
155
|
+
expect(expected).toBe(FILE_UPLOADED_EVENT_TYPE);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { fileUploadedPayloadSchema } from "@cosmicdrift/kumiko-framework/files";
|
|
3
|
+
import { fileRefEntity } from "./schema/file-ref";
|
|
4
|
+
|
|
5
|
+
export { fileRefEntity } from "./schema/file-ref";
|
|
6
|
+
|
|
7
|
+
// files — Schema-Sicht der framework-internen file_refs-Tabelle als
|
|
8
|
+
// bundled-feature, damit Cross-Feature-Hooks (userData, tenantData) sich
|
|
9
|
+
// an die "fileRef"-Entity hängen können.
|
|
10
|
+
//
|
|
11
|
+
// Sprint 1.5 (this commit):
|
|
12
|
+
// - r.entity("fileRef", fileRefEntity) — Schema-Surface
|
|
13
|
+
// - r.defineEvent("uploaded", schema) — Event-Marker
|
|
14
|
+
//
|
|
15
|
+
// Sprint 2 (kommt):
|
|
16
|
+
// - r.useExtension(EXT_USER_DATA, "fileRef", { export, delete })
|
|
17
|
+
//
|
|
18
|
+
// Sprint 5 (kommt):
|
|
19
|
+
// - r.useExtension(EXT_TENANT_DATA, "fileRef", { destroy })
|
|
20
|
+
//
|
|
21
|
+
// Routes bleiben framework-internal (multipart-Upload + binary-Streaming
|
|
22
|
+
// passen nicht in das Handler-Pattern; siehe schema/file-ref.ts für
|
|
23
|
+
// Architektur-Note).
|
|
24
|
+
//
|
|
25
|
+
// Sprint-1.5-Plan-Roadmap-Wille: "fileRefsTable bleibt in framework
|
|
26
|
+
// (kein Daten-Move), aber r.entity('fileRef') deklariert sie für das
|
|
27
|
+
// Feature." — diese Datei IST die Umsetzung.
|
|
28
|
+
export function createFilesFeature(): FeatureDefinition {
|
|
29
|
+
return defineFeature("files", (r) => {
|
|
30
|
+
r.entity("fileRef", fileRefEntity);
|
|
31
|
+
|
|
32
|
+
r.defineEvent("uploaded", fileUploadedPayloadSchema);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createFilesFeature, fileRefEntity } from "./feature";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEntity,
|
|
3
|
+
createNumberField,
|
|
4
|
+
createTextField,
|
|
5
|
+
createTimestampField,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
|
|
8
|
+
// fileRef — Schema-Sicht der File-Metadata-Tabelle aus dem Framework.
|
|
9
|
+
//
|
|
10
|
+
// Architektur-Entscheidung (Sprint 1.5):
|
|
11
|
+
//
|
|
12
|
+
// Die DB-Tabelle `file_refs` lebt weiterhin in
|
|
13
|
+
// `framework/src/files/file-ref-table.ts` als drizzle pgTable, weil
|
|
14
|
+
// die Hono-Upload-/Download-Routes (`createFileRoutes` in
|
|
15
|
+
// `framework/src/api/server.ts`) sie direkt nutzen. Multipart-Upload
|
|
16
|
+
// und Binary-Streaming passen nicht in das Write/Query-Handler-Pattern
|
|
17
|
+
// — Routes bleiben framework-internal.
|
|
18
|
+
//
|
|
19
|
+
// Was hier passiert: dieselbe DB-Tabelle wird zusätzlich als
|
|
20
|
+
// `r.entity("fileRef")` in einem bundled-feature deklariert. Das
|
|
21
|
+
// ermoeglicht:
|
|
22
|
+
// 1. r.useExtension(EXT_USER_DATA, "fileRef", { export, delete })
|
|
23
|
+
// in Sprint 2 — Forget-Flow + Daten-Export erkennen die Entity.
|
|
24
|
+
// 2. r.useExtension(EXT_TENANT_DATA, "fileRef", { destroy })
|
|
25
|
+
// in Sprint 5 — Tenant-Lifecycle löscht alle FileRefs.
|
|
26
|
+
// 3. Boot-Validation für PII-Annotations greift (fileName, originalName).
|
|
27
|
+
//
|
|
28
|
+
// Kein buildDrizzleTable hier — die Mapping-Tabelle existiert schon im
|
|
29
|
+
// Framework. Drizzle-Reads in den Sprint-2+-Hooks gehen direkt über
|
|
30
|
+
// `fileRefsTable` aus `@cosmicdrift/kumiko-framework/files`.
|
|
31
|
+
//
|
|
32
|
+
// PII-Annotations (Sprint 0.1+0.7+1.7):
|
|
33
|
+
// - fileName → pii: true (Originalname enthält oft Personen-Bezug:
|
|
34
|
+
// "Marc-Lebenslauf.pdf", "Krankheitsattest-Mai.pdf")
|
|
35
|
+
//
|
|
36
|
+
// Andere Felder brauchen KEINE Annotation:
|
|
37
|
+
// - storageKey, mimeType, size, entityType, entityId, fieldName,
|
|
38
|
+
// insertedById → keine PII-typischen Field-Namen, PII-Heuristik
|
|
39
|
+
// greift nicht (siehe boot-validator.ts PII_DIRECT_NAME_HINTS).
|
|
40
|
+
// Ein allowPlaintext-Marker wäre Über-Annotation ohne Effekt.
|
|
41
|
+
// - insertedAt → Audit-Timestamp, framework-managed.
|
|
42
|
+
//
|
|
43
|
+
// Tabellenname matched die Framework-pgTable damit r.entity-Reads über
|
|
44
|
+
// dieselbe Postgres-Tabelle laufen.
|
|
45
|
+
export const fileRefEntity = createEntity({
|
|
46
|
+
table: "file_refs",
|
|
47
|
+
fields: {
|
|
48
|
+
storageKey: createTextField({ required: true }),
|
|
49
|
+
fileName: createTextField({ required: true, pii: true }),
|
|
50
|
+
mimeType: createTextField({ required: true }),
|
|
51
|
+
size: createNumberField({ required: true }),
|
|
52
|
+
entityType: createTextField(),
|
|
53
|
+
entityId: createTextField(),
|
|
54
|
+
fieldName: createTextField(),
|
|
55
|
+
insertedAt: createTimestampField({ sortable: true, filterable: true }),
|
|
56
|
+
insertedById: createTextField(),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
1
2
|
import {
|
|
2
3
|
DeleteObjectCommand,
|
|
3
4
|
GetObjectCommand,
|
|
@@ -5,9 +6,45 @@ import {
|
|
|
5
6
|
PutObjectCommand,
|
|
6
7
|
S3Client,
|
|
7
8
|
} from "@aws-sdk/client-s3";
|
|
9
|
+
import { Upload } from "@aws-sdk/lib-storage";
|
|
8
10
|
import { getSignedUrl as presign } from "@aws-sdk/s3-request-presigner";
|
|
9
11
|
import type { FileStorageProvider, SignedUrlOptions } from "@cosmicdrift/kumiko-framework/files";
|
|
10
12
|
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Operator-Pflicht-Setup (Multipart-Upload-Cleanup)
|
|
15
|
+
// =============================================================================
|
|
16
|
+
//
|
|
17
|
+
// `writeStream` nutzt @aws-sdk/lib-storage's Upload-class fuer echtes
|
|
18
|
+
// multipart-streaming. S3 created dabei eine Multipart-Upload-Session mit
|
|
19
|
+
// einer Upload-ID; bei normaler Completion wird sie via Complete-
|
|
20
|
+
// MultipartUpload geschlossen.
|
|
21
|
+
//
|
|
22
|
+
// **Edge-Case bei Worker-Abort:** wenn der Export-Worker mid-write gecancelt
|
|
23
|
+
// wird (Pod-Restart, K8s-OOM-Kill, Process-Signal), bleibt die Multipart-
|
|
24
|
+
// Upload-Session in S3 OFFEN. S3 behaelt die bereits hochgeladenen Parts
|
|
25
|
+
// und berechnet Storage-Kosten dafuer — bis sie manuell oder via Lifecycle-
|
|
26
|
+
// Rule abgebrochen werden.
|
|
27
|
+
//
|
|
28
|
+
// **Pflicht-Operator-Setup auf jedem Bucket:**
|
|
29
|
+
//
|
|
30
|
+
// {
|
|
31
|
+
// "Rules": [{
|
|
32
|
+
// "ID": "AbortIncompleteMultipartUploads",
|
|
33
|
+
// "Status": "Enabled",
|
|
34
|
+
// "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 },
|
|
35
|
+
// "Filter": {}
|
|
36
|
+
// }]
|
|
37
|
+
// }
|
|
38
|
+
//
|
|
39
|
+
// AWS-CLI: `aws s3api put-bucket-lifecycle-configuration --bucket <name>
|
|
40
|
+
// --lifecycle-configuration file://lifecycle.json`. Hetzner Object Storage
|
|
41
|
+
// + R2 + Minio supporten dieselbe Syntax.
|
|
42
|
+
//
|
|
43
|
+
// **Code-side abort()** fuer graceful Worker-Shutdown ist follow-up. Das
|
|
44
|
+
// braucht Worker-Cancel-Semantik (AbortSignal-Propagation durch r.job),
|
|
45
|
+
// die im framework noch nicht existiert. Bis dahin ist die Lifecycle-
|
|
46
|
+
// Rule die einzige Garantie gegen Storage-Leakage.
|
|
47
|
+
|
|
11
48
|
// Minimal config surface — everything the SDK needs, nothing framework-
|
|
12
49
|
// specific. Apps wire this into `buildServer({ files: { storageProvider } })`
|
|
13
50
|
// the same way they'd pass createLocalProvider in dev.
|
|
@@ -62,6 +99,33 @@ export function createS3Provider(config: S3ProviderConfig): FileStorageProvider
|
|
|
62
99
|
);
|
|
63
100
|
},
|
|
64
101
|
|
|
102
|
+
async writeStream(key, source, options): Promise<void> {
|
|
103
|
+
// Echtes multipart-streaming via @aws-sdk/lib-storage.Upload —
|
|
104
|
+
// der Source-AsyncIterable wird chunk-weise zu S3 hochgeladen,
|
|
105
|
+
// niemals alles im Memory aggregiert. lib-storage handled
|
|
106
|
+
// automatisch chunking (5MB-Parts default), parallel-uploads
|
|
107
|
+
// (4 concurrent default), und retry bei Part-Failures.
|
|
108
|
+
//
|
|
109
|
+
// Memory-Footprint: ~5MB pro in-flight-part × 4 concurrent =
|
|
110
|
+
// ~20MB Heap-Bound, unabhaengig von der Total-Bundle-Size. Macht
|
|
111
|
+
// 1GB+ Bundles moeglich ohne OOM.
|
|
112
|
+
//
|
|
113
|
+
// Readable.from(source) adapiert AsyncIterable → node:Readable —
|
|
114
|
+
// lib-storage's Body-Type akzeptiert Web-ReadableStream + node-
|
|
115
|
+
// Readable, nicht direkt AsyncIterable. Adapter ist zero-copy.
|
|
116
|
+
const body = Readable.from(source);
|
|
117
|
+
const upload = new Upload({
|
|
118
|
+
client,
|
|
119
|
+
params: {
|
|
120
|
+
Bucket: config.bucket,
|
|
121
|
+
Key: key,
|
|
122
|
+
Body: body,
|
|
123
|
+
...(options?.mimeType !== undefined && { ContentType: options.mimeType }),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
await upload.done();
|
|
127
|
+
},
|
|
128
|
+
|
|
65
129
|
async read(key): Promise<Uint8Array> {
|
|
66
130
|
const response = await client.send(new GetObjectCommand({ Bucket: config.bucket, Key: key }));
|
|
67
131
|
if (!response.Body) {
|
|
@@ -73,6 +137,31 @@ export function createS3Provider(config: S3ProviderConfig): FileStorageProvider
|
|
|
73
137
|
return response.Body.transformToByteArray();
|
|
74
138
|
},
|
|
75
139
|
|
|
140
|
+
readStream(key): AsyncIterable<Uint8Array> {
|
|
141
|
+
// S3 GetObject.Body ist ein StreamingBlobPayloadOutputTypes — auf
|
|
142
|
+
// node ist das ein Readable-Stream der bereits AsyncIterable<Buffer>
|
|
143
|
+
// ist. Wir wrappen lazy: erst beim ersten chunk-pull wird der
|
|
144
|
+
// GetObject-Request abgesetzt. Wenn der Key nicht existiert, faellt
|
|
145
|
+
// der Error genau dort (nicht beim readStream-Aufruf) — gleiches
|
|
146
|
+
// Lazy-Verhalten wie inmemory + local.
|
|
147
|
+
return {
|
|
148
|
+
async *[Symbol.asyncIterator]() {
|
|
149
|
+
const response = await client.send(
|
|
150
|
+
new GetObjectCommand({ Bucket: config.bucket, Key: key }),
|
|
151
|
+
);
|
|
152
|
+
if (!response.Body) {
|
|
153
|
+
throw new Error(`s3_read_empty_body: ${key}`);
|
|
154
|
+
}
|
|
155
|
+
// SdkStream is AsyncIterable<Buffer> on node. Buffer extends
|
|
156
|
+
// Uint8Array; cast sichert die Surface ohne neue runtime-deps.
|
|
157
|
+
const body = response.Body as AsyncIterable<Uint8Array>; // @cast-boundary engine-bridge
|
|
158
|
+
for await (const chunk of body) {
|
|
159
|
+
yield chunk;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
|
|
76
165
|
async delete(key): Promise<void> {
|
|
77
166
|
await client.send(new DeleteObjectCommand({ Bucket: config.bucket, Key: key }));
|
|
78
167
|
},
|
|
@@ -85,7 +174,7 @@ export function createS3Provider(config: S3ProviderConfig): FileStorageProvider
|
|
|
85
174
|
// S3 SDK throws either NotFound or a generic 404. Check both the
|
|
86
175
|
// `.name` property (newer SDKs) and the `$metadata.httpStatusCode`
|
|
87
176
|
// (what the SDK guarantees on every error).
|
|
88
|
-
const err = error as { name?: string; $metadata?: { httpStatusCode?: number } };
|
|
177
|
+
const err = error as { name?: string; $metadata?: { httpStatusCode?: number } }; // @cast-boundary error-details
|
|
89
178
|
if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) {
|
|
90
179
|
return false;
|
|
91
180
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { and, desc, eq } from "drizzle-orm";
|
|
2
|
+
import { and, desc, eq, type SQL } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { type JobRunStatus, jobRunsTable } from "../job-run-table";
|
|
5
5
|
|
|
@@ -13,13 +13,13 @@ export const listQuery = defineQueryHandler({
|
|
|
13
13
|
access: { roles: ["SystemAdmin"] },
|
|
14
14
|
handler: async (query, ctx) => {
|
|
15
15
|
const db = ctx.db;
|
|
16
|
-
const conditions = [];
|
|
16
|
+
const conditions: SQL[] = [];
|
|
17
17
|
|
|
18
18
|
if (query.payload.jobName) {
|
|
19
19
|
conditions.push(eq(jobRunsTable.jobName, query.payload.jobName));
|
|
20
20
|
}
|
|
21
21
|
if (query.payload.status) {
|
|
22
|
-
conditions.push(eq(jobRunsTable.status, query.payload.status as JobRunStatus));
|
|
22
|
+
conditions.push(eq(jobRunsTable.status, query.payload.status as JobRunStatus)); // @cast-boundary engine-payload
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const limit = query.payload.limit ?? 50;
|
|
@@ -14,7 +14,7 @@ export const triggerWrite = defineWriteHandler({
|
|
|
14
14
|
handler: async (event, ctx) => {
|
|
15
15
|
const registry = ctx.registry;
|
|
16
16
|
// `jobRunner` is a dynamic context extension — not a core HandlerContext field.
|
|
17
|
-
const jobRunner = ctx["jobRunner"] as JobRunner;
|
|
17
|
+
const jobRunner = ctx["jobRunner"] as JobRunner; // @cast-boundary dynamic-key
|
|
18
18
|
|
|
19
19
|
const jobDef = registry.getJob(event.payload.jobName);
|
|
20
20
|
if (!jobDef) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Client-Feature-Factory für legal-pages Visual-Tree. Liefert statische
|
|
3
|
+
// Tree-Knoten für die DACH-Compliance-Blöcke (imprint, privacy in de/en).
|
|
4
|
+
// Jeder Knoten linkt auf text-content's edit-Action — reines Cross-Feature-
|
|
5
|
+
// Linking, kein eigener State oder Fetch nötig.
|
|
6
|
+
//
|
|
7
|
+
// **Static statt fetch**: legal-pages weiß out-of-the-box welche Blocks
|
|
8
|
+
// existieren (LEGAL_REQUIRED_BLOCKS + LEGAL_OPTIONAL_BLOCKS aus constants).
|
|
9
|
+
// Anders als text-content's Provider (der alle Slugs des Tenants holt)
|
|
10
|
+
// ist diese Liste bekannt zur Build-Zeit — kein /api/query-Round-trip nötig.
|
|
11
|
+
//
|
|
12
|
+
// **Content-State unbekannt**: V.1.2 setzt keine state-Markierung; alle
|
|
13
|
+
// Knoten erscheinen "filled" (default). V.1.3+ könnte via by-slug-Query
|
|
14
|
+
// ermitteln ob ein Block tatsächlich body hat und „stub" markieren wenn
|
|
15
|
+
// leer (Provider-Author-Hinweis dass Block existiert aber befüllt werden
|
|
16
|
+
// muss). Aktuell ist legal-pages's Boot-Check der primäre Wächter für
|
|
17
|
+
// fehlende Pflicht-Blocks.
|
|
18
|
+
|
|
19
|
+
import type { TreeChildrenSubscribe, TreeNode } from "@cosmicdrift/kumiko-framework/engine";
|
|
20
|
+
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
21
|
+
import { LEGAL_OPTIONAL_BLOCKS, LEGAL_REQUIRED_BLOCKS } from "../constants";
|
|
22
|
+
|
|
23
|
+
const treeProvider: TreeChildrenSubscribe = (_ctx) => (emit) => {
|
|
24
|
+
const allBlocks = [...LEGAL_REQUIRED_BLOCKS, ...LEGAL_OPTIONAL_BLOCKS];
|
|
25
|
+
const nodes: readonly TreeNode[] = allBlocks.map((b) => ({
|
|
26
|
+
label: `${b.slug} (${b.lang})`,
|
|
27
|
+
target: {
|
|
28
|
+
featureId: "text-content",
|
|
29
|
+
action: "edit",
|
|
30
|
+
args: { slug: b.slug, lang: b.lang },
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
emit(nodes);
|
|
34
|
+
return () => {};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function legalPagesClient(): ClientFeatureDefinition {
|
|
38
|
+
return {
|
|
39
|
+
name: "legal-pages",
|
|
40
|
+
treeProvider,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -134,7 +134,7 @@ export async function createTransportForTenant(
|
|
|
134
134
|
await ctxConfig(mailFoundationFeature.exports.configKeys.provider),
|
|
135
135
|
FEATURE_NAME,
|
|
136
136
|
"provider",
|
|
137
|
-
) as string;
|
|
137
|
+
) as string; // @cast-boundary engine-payload
|
|
138
138
|
if (provider.length === 0) {
|
|
139
139
|
const usages = ctx.registry.getExtensionUsages("mailTransport");
|
|
140
140
|
const known = usages.map((u) => u.entityName).join(", ") || "<none>";
|
|
@@ -140,12 +140,12 @@ async function buildSmtpTransport(ctx: HandlerContext, tenantId: string): Promis
|
|
|
140
140
|
await ctxConfig(mailTransportSmtpFeature.exports.configKeys.port),
|
|
141
141
|
FEATURE_NAME,
|
|
142
142
|
"port",
|
|
143
|
-
) as number;
|
|
143
|
+
) as number; // @cast-boundary engine-payload
|
|
144
144
|
const secure = requireDefined(
|
|
145
145
|
await ctxConfig(mailTransportSmtpFeature.exports.configKeys.secure),
|
|
146
146
|
FEATURE_NAME,
|
|
147
147
|
"secure",
|
|
148
|
-
) as boolean;
|
|
148
|
+
) as boolean; // @cast-boundary engine-payload
|
|
149
149
|
const from = requireNonEmpty(
|
|
150
150
|
await ctxConfig(mailTransportSmtpFeature.exports.configKeys.from),
|
|
151
151
|
FEATURE_NAME,
|
|
@@ -38,7 +38,7 @@ export const simpleRenderer: NotificationRenderer = {
|
|
|
38
38
|
name: "simple",
|
|
39
39
|
|
|
40
40
|
async render(input) {
|
|
41
|
-
const data = input.variables as EmailTemplateData;
|
|
41
|
+
const data = input.variables as EmailTemplateData; // @cast-boundary render-helper
|
|
42
42
|
|
|
43
43
|
// Fallback: if no structured fields, use title + body as header + single text section
|
|
44
44
|
const header = data.header ?? data.title;
|