@cosmicdrift/kumiko-bundled-features 0.35.0 → 0.38.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.
Files changed (48) hide show
  1. package/package.json +5 -5
  2. package/src/auth-email-password/__tests__/auth-mailer.test.ts +138 -0
  3. package/src/auth-email-password/auth-mailer.ts +137 -0
  4. package/src/auth-email-password/email-templates.ts +7 -13
  5. package/src/auth-email-password/errors.ts +84 -0
  6. package/src/auth-email-password/handlers/change-password.write.ts +1 -10
  7. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +3 -19
  8. package/src/auth-email-password/handlers/invite-accept.write.ts +15 -28
  9. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +2 -14
  10. package/src/auth-email-password/handlers/login.write.ts +7 -51
  11. package/src/auth-email-password/handlers/reset-password.write.ts +3 -10
  12. package/src/auth-email-password/handlers/signup-confirm.write.ts +2 -14
  13. package/src/auth-email-password/handlers/verify-email.write.ts +3 -10
  14. package/src/auth-email-password/index.ts +9 -0
  15. package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +24 -0
  16. package/src/auth-email-password/web/forgot-password-screen.tsx +1 -0
  17. package/src/auth-email-password/web/tenant-switcher.tsx +2 -1
  18. package/src/cap-counter/enforce-cap.ts +5 -0
  19. package/src/compliance-profiles/README.md +1 -1
  20. package/src/custom-fields/__tests__/feature.test.ts +1 -1
  21. package/src/custom-fields/__tests__/wire-for-entity.test.ts +4 -4
  22. package/src/custom-fields/db/queries/retention.ts +1 -0
  23. package/src/custom-fields/lib/parse-serialized-field.ts +11 -0
  24. package/src/custom-fields/run-retention.ts +4 -22
  25. package/src/custom-fields/web/__tests__/custom-fields-form-section.test.tsx +148 -0
  26. package/src/custom-fields/web/custom-fields-form-section.tsx +26 -12
  27. package/src/custom-fields/wire-for-entity.ts +4 -12
  28. package/src/custom-fields/wire-user-data-rights.ts +3 -22
  29. package/src/data-retention/__tests__/data-retention.integration.test.ts +2 -2
  30. package/src/file-foundation/feature.ts +13 -3
  31. package/src/file-foundation/index.ts +1 -0
  32. package/src/file-provider-inmemory/__tests__/feature.test.ts +4 -7
  33. package/src/file-provider-s3/__tests__/feature.test.ts +4 -6
  34. package/src/files/README.md +1 -1
  35. package/src/legal-pages/markdown.ts +1 -13
  36. package/src/renderer-simple/simple-renderer.ts +1 -8
  37. package/src/subscription-stripe/feature.ts +5 -2
  38. package/src/template-resolver/feature.ts +1 -2
  39. package/src/template-resolver/handlers/list.query.ts +7 -14
  40. package/src/template-resolver/handlers/toggle-status.write.ts +37 -0
  41. package/src/tenant/command-schemas.ts +1 -1
  42. package/src/tenant/feature.ts +1 -2
  43. package/src/tenant/handlers/toggle-enabled.write.ts +23 -0
  44. package/src/user-data-rights/README.md +8 -8
  45. package/src/template-resolver/handlers/archive.write.ts +0 -39
  46. package/src/template-resolver/handlers/publish.write.ts +0 -42
  47. package/src/tenant/handlers/disable.write.ts +0 -18
  48. package/src/tenant/handlers/enable.write.ts +0 -20
@@ -1,5 +1,6 @@
1
1
  // T1.5c — user-data-rights wiring for custom-fields.
2
2
 
3
+ import { extractTableName } from "@cosmicdrift/kumiko-framework/db";
3
4
  import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
4
5
  import { EXT_USER_DATA, type FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
5
6
  import {
@@ -7,16 +8,7 @@ import {
7
8
  selectFieldDefinitionsForEntity,
8
9
  stripSensitiveCustomFieldKeys,
9
10
  } from "./db/queries/user-data-rights";
10
- import { parseSerializedField } from "./lib/parse-serialized-field";
11
-
12
- const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
13
- function getTableName(table: unknown): string {
14
- if (typeof table === "object" && table !== null) {
15
- const sym = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
16
- if (typeof sym === "string") return sym;
17
- }
18
- throw new Error("wire-user-data-rights: table missing kumiko:schema:Name symbol");
19
- }
11
+ import { isFieldDefinitionRow, parseSerializedField } from "./lib/parse-serialized-field";
20
12
 
21
13
  export interface WireCustomFieldsUserDataRightsOptions {
22
14
  readonly entityName: string;
@@ -52,7 +44,7 @@ export function wireCustomFieldsUserDataRightsFor<TReg extends FeatureRegistrar<
52
44
  r: TReg,
53
45
  opts: WireCustomFieldsUserDataRightsOptions,
54
46
  ): void {
55
- const tableName = getTableName(opts.entityTable);
47
+ const tableName = extractTableName(opts.entityTable, "custom-fields/wire-user-data-rights");
56
48
 
57
49
  const exportHook: UserDataExportHook = async (ctx) => {
58
50
  const rows = await selectCustomFieldsHostRows(
@@ -100,17 +92,6 @@ export function wireCustomFieldsUserDataRightsFor<TReg extends FeatureRegistrar<
100
92
  });
101
93
  }
102
94
 
103
- interface FieldDefinitionRow {
104
- readonly field_key: string;
105
- readonly serialized_field: unknown;
106
- }
107
-
108
- function isFieldDefinitionRow(value: unknown): value is FieldDefinitionRow {
109
- if (!value || typeof value !== "object") return false;
110
- if (!("field_key" in value)) return false;
111
- return typeof value.field_key === "string";
112
- }
113
-
114
95
  async function loadSensitiveFieldKeys(
115
96
  db: Parameters<UserDataExportHook>[0]["db"],
116
97
  tenantId: string,
@@ -36,11 +36,11 @@ describe("data-retention :: feature-definition smoke", () => {
36
36
  });
37
37
 
38
38
  test("tenantRetentionOverride-Entity ist registriert", () => {
39
- expect(feature.entities["tenant-retention-override"]).toBeDefined();
39
+ expect(feature.entities?.["tenant-retention-override"]).toBeDefined();
40
40
  });
41
41
 
42
42
  test("Entity-Definition hat UNIQUE(tenantId, entityName) als 1:1-Constraint", () => {
43
- const entity = feature.entities["tenant-retention-override"];
43
+ const entity = feature.entities?.["tenant-retention-override"];
44
44
  const indexes = entity?.indexes ?? [];
45
45
  const uniqueIndex = indexes.find((i) => i.unique === true);
46
46
  expect(uniqueIndex).toBeDefined();
@@ -85,6 +85,12 @@ export type FileProviderPlugin = {
85
85
  readonly build: (ctx: FileProviderContext, tenantId: string) => Promise<FileStorageProvider>;
86
86
  };
87
87
 
88
+ // extension-usage `options` is engine-payload (unknown) — structurally validate
89
+ // instead of casting blind.
90
+ export function isFileProviderPlugin(o: unknown): o is FileProviderPlugin {
91
+ return typeof o === "object" && o !== null && "build" in o && typeof o.build === "function";
92
+ }
93
+
88
94
  // =============================================================================
89
95
  // Feature-definition
90
96
  // =============================================================================
@@ -163,7 +169,11 @@ export async function createFileProviderForTenant(
163
169
  );
164
170
  }
165
171
 
166
- // @cast-boundary engine-payload — extension-usage carries unknown options
167
- const plugin = usage.options as FileProviderPlugin;
168
- return plugin.build(ctx, tenantId);
172
+ if (!isFileProviderPlugin(usage.options)) {
173
+ throw new Error(
174
+ `${FEATURE_NAME}: provider "${provider}" registered without a build() — ` +
175
+ `extension options must be a FileProviderPlugin.`,
176
+ );
177
+ }
178
+ return usage.options.build(ctx, tenantId);
169
179
  }
@@ -5,4 +5,5 @@ export {
5
5
  type FileProviderContext,
6
6
  type FileProviderPlugin,
7
7
  fileFoundationFeature,
8
+ isFileProviderPlugin,
8
9
  } from "./feature";
@@ -1,7 +1,10 @@
1
1
  // feature.ts contract tests for file-provider-inmemory.
2
2
 
3
3
  import { describe, expect, test } from "bun:test";
4
- import type { FileProviderPlugin } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
4
+ import {
5
+ type FileProviderPlugin,
6
+ isFileProviderPlugin,
7
+ } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
5
8
  import { clearStorage, fileProviderInMemoryFeature, listKeys } from "../feature";
6
9
 
7
10
  describe("fileProviderInMemoryFeature — shape", () => {
@@ -35,12 +38,6 @@ describe("listKeys / clearStorage — per-tenant store helpers", () => {
35
38
  });
36
39
  });
37
40
 
38
- // extension-usage `options` is engine-payload (unknown) — structurally validate
39
- // instead of casting blind.
40
- function isFileProviderPlugin(o: unknown): o is FileProviderPlugin {
41
- return typeof o === "object" && o !== null && "build" in o && typeof o.build === "function";
42
- }
43
-
44
41
  function inmemoryPlugin(): FileProviderPlugin {
45
42
  const options = fileProviderInMemoryFeature.extensionUsages.find(
46
43
  (u) => u.extensionName === "fileProvider" && u.entityName === "inmemory",
@@ -1,7 +1,10 @@
1
1
  // feature.ts contract tests for file-provider-s3.
2
2
 
3
3
  import { describe, expect, test } from "bun:test";
4
- import type { FileProviderPlugin } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
4
+ import {
5
+ type FileProviderPlugin,
6
+ isFileProviderPlugin,
7
+ } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
5
8
  import { fileProviderS3Feature, S3_SECRET_ACCESS_KEY } from "../feature";
6
9
 
7
10
  describe("fileProviderS3Feature — shape", () => {
@@ -54,11 +57,6 @@ describe("fileProviderS3Feature — plugin-registration", () => {
54
57
  });
55
58
  });
56
59
 
57
- // extension-usage `options` is engine-payload (unknown) — structurally validate.
58
- function isFileProviderPlugin(o: unknown): o is FileProviderPlugin {
59
- return typeof o === "object" && o !== null && "build" in o && typeof o.build === "function";
60
- }
61
-
62
60
  function s3Plugin(): FileProviderPlugin {
63
61
  const options = fileProviderS3Feature.extensionUsages.find(
64
62
  (u) => u.extensionName === "fileProvider" && u.entityName === "s3",
@@ -46,5 +46,5 @@ Sprint 4).
46
46
 
47
47
  ## Tests
48
48
 
49
- `__tests__/files.integration.ts` — 5 Tests die beweisen dass die Feature-
49
+ `__tests__/files.integration.test.ts` — 5 Tests die beweisen dass die Feature-
50
50
  Definition clean lädt + die PII-Markers + Tabellenname stimmen.
@@ -1,3 +1,4 @@
1
+ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
1
2
  import { Marked } from "marked";
2
3
 
3
4
  // Markdown→HTML mit eigener `marked`-Instance. GFM aus, breaks aus —
@@ -54,16 +55,3 @@ ${opts.bodyHtml}
54
55
  </body>
55
56
  </html>`;
56
57
  }
57
-
58
- function escapeHtml(s: string): string {
59
- return s
60
- .replace(/&/g, "&amp;")
61
- .replace(/</g, "&lt;")
62
- .replace(/>/g, "&gt;")
63
- .replace(/"/g, "&quot;")
64
- .replace(/'/g, "&#39;");
65
- }
66
-
67
- function escapeHtmlAttr(s: string): string {
68
- return escapeHtml(s);
69
- }
@@ -1,3 +1,4 @@
1
+ import { escapeHtml } from "@cosmicdrift/kumiko-headless";
1
2
  import type { NotificationRenderer } from "../delivery";
2
3
 
3
4
  type Section =
@@ -14,14 +15,6 @@ type EmailTemplateData = {
14
15
  readonly body?: string;
15
16
  };
16
17
 
17
- function escapeHtml(str: string): string {
18
- return str
19
- .replace(/&/g, "&amp;")
20
- .replace(/</g, "&lt;")
21
- .replace(/>/g, "&gt;")
22
- .replace(/"/g, "&quot;");
23
- }
24
-
25
18
  function renderSection(section: Section): string {
26
19
  if ("text" in section) {
27
20
  return `<p style="margin:0 0 16px;color:#333;font-size:14px;line-height:1.5">${escapeHtml(section.text)}</p>`;
@@ -65,8 +65,11 @@ export const subscriptionStripeEnvSchema = z.object({
65
65
  .meta({ kumiko: { pulumi: { secret: true } } }),
66
66
  STRIPE_API_KEY: z
67
67
  .string()
68
- .regex(/^sk_(test|live)_/, "STRIPE_API_KEY must start with 'sk_test_' or 'sk_live_'")
69
- .describe("Stripe API key (`sk_live_...` / `sk_test_...`).")
68
+ .regex(
69
+ /^(sk|rk)_(test|live)_/,
70
+ "STRIPE_API_KEY must start with 'sk_test_'/'sk_live_' or a restricted 'rk_test_'/'rk_live_' key",
71
+ )
72
+ .describe("Stripe API key (`sk_live_...` / `sk_test_...`, restricted `rk_...` keys allowed).")
70
73
  .meta({ kumiko: { pulumi: { secret: true } } }),
71
74
  });
72
75
 
@@ -1,8 +1,7 @@
1
1
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
2
- import { archiveWrite } from "./handlers/archive.write";
3
2
  import { findByIdQuery } from "./handlers/find-by-id.query";
4
3
  import { listQuery } from "./handlers/list.query";
5
- import { publishWrite } from "./handlers/publish.write";
4
+ import { archiveWrite, publishWrite } from "./handlers/toggle-status.write";
6
5
  import { upsertSystemWrite } from "./handlers/upsert-system.write";
7
6
  import { upsertTenantWrite } from "./handlers/upsert-tenant.write";
8
7
  import { templateResourceEntity } from "./table";
@@ -1,5 +1,5 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { z } from "zod";
4
4
  import { RENDER_KINDS, TEMPLATE_STATUSES } from "../constants";
5
5
  import { type TemplateResourceRow, templateResourcesTable } from "../table";
@@ -17,14 +17,13 @@ export const listQuery = defineQueryHandler({
17
17
  }),
18
18
  access: { roles: ["TenantAdmin", "SystemAdmin", "User"] },
19
19
  handler: async (query, ctx) => {
20
- const isSystemAdmin = query.user.roles.includes("SystemAdmin");
21
20
  const where: Record<string, unknown> = {};
22
21
 
23
- // TenantDb scopes non-SystemAdmin reads to [own tenant, SYSTEM reference] and
24
- // refuses a caller-narrowed where.tenantId (enforced isolation). SystemAdmin
25
- // uses a system-scoped db that sees every tenant, so narrow to own explicitly
26
- // when they don't want the cross-tenant view.
27
- if (isSystemAdmin && !query.payload.includeSystem) {
22
+ // includeSystem=false narrows to own tenant AT THE DB — for non-SystemAdmin
23
+ // TenantDb permits narrowing within its enforced [own, SYSTEM] scope, for
24
+ // SystemAdmin (system-scoped db) the where applies verbatim. Filtering at
25
+ // the DB keeps the limit meaningful (no post-filter starvation).
26
+ if (!query.payload.includeSystem) {
28
27
  where["tenantId"] = query.user.tenantId;
29
28
  }
30
29
 
@@ -44,13 +43,7 @@ export const listQuery = defineQueryHandler({
44
43
  limit: 500,
45
44
  })) as TemplateResourceRow[];
46
45
 
47
- // TenantDb always surfaces SYSTEM reference rows alongside the tenant's own;
48
- // includeSystem=false drops them here since they can't be excluded at the DB.
49
- const visible = query.payload.includeSystem
50
- ? rows
51
- : rows.filter((row) => row.tenantId !== SYSTEM_TENANT_ID);
52
-
53
- return visible.map((row) => ({
46
+ return rows.map((row) => ({
54
47
  id: String(row.id),
55
48
  tenantId: row.tenantId,
56
49
  slug: row.slug,
@@ -0,0 +1,37 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import { z } from "zod";
5
+ import type { TemplateResourceRow } from "../table";
6
+ import { templateResourcesTable } from "../table";
7
+ import { executor } from "./shared";
8
+
9
+ type TemplateStatus = "active" | "archived";
10
+
11
+ function createStatusUpdateHandler(name: string, status: TemplateStatus) {
12
+ return defineWriteHandler({
13
+ name,
14
+ schema: z.object({ id: z.string().min(1) }),
15
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
16
+ handler: async (event, ctx) => {
17
+ // ctx.db is tenant-scoped: a foreign tenant's id reads as absent → NotFound.
18
+ // Cross-tenant toggling needs SystemAdmin with tenantIdOverride.
19
+ const existing = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
20
+ id: event.payload.id,
21
+ });
22
+ if (!existing) {
23
+ return writeFailure(new NotFoundError("template-resource", event.payload.id));
24
+ }
25
+ const result = await executor.update(
26
+ { id: existing.id, version: existing.version, changes: { status } },
27
+ event.user,
28
+ ctx.db,
29
+ );
30
+ if (!result.isSuccess) return result;
31
+ return { isSuccess: true as const, data: { id: String(existing.id), status } };
32
+ },
33
+ });
34
+ }
35
+
36
+ export const archiveWrite = createStatusUpdateHandler("archive", "archived");
37
+ export const publishWrite = createStatusUpdateHandler("publish", "active");
@@ -22,8 +22,8 @@
22
22
 
23
23
  import { addMemberWrite } from "./handlers/add-member.write";
24
24
  import { createWrite } from "./handlers/create.write";
25
- import { disableWrite } from "./handlers/disable.write";
26
25
  import { removeMemberWrite } from "./handlers/remove-member.write";
26
+ import { disableWrite } from "./handlers/toggle-enabled.write";
27
27
  import { updateWrite } from "./handlers/update.write";
28
28
  import { updateMemberRolesWrite } from "./handlers/update-member-roles.write";
29
29
 
@@ -9,8 +9,6 @@ import { activeTenantIdsQuery } from "./handlers/active-tenant-ids.query";
9
9
  import { addMemberWrite } from "./handlers/add-member.write";
10
10
  import { cancelInvitationWrite } from "./handlers/cancel-invitation.write";
11
11
  import { createWrite } from "./handlers/create.write";
12
- import { disableWrite } from "./handlers/disable.write";
13
- import { enableWrite } from "./handlers/enable.write";
14
12
  import { invitationsQuery } from "./handlers/invitations.query";
15
13
  import { listQuery } from "./handlers/list.query";
16
14
  import { meQuery } from "./handlers/me.query";
@@ -18,6 +16,7 @@ import { membersQuery } from "./handlers/members.query";
18
16
  import { membershipsQuery } from "./handlers/memberships.query";
19
17
  import { removeMemberWrite } from "./handlers/remove-member.write";
20
18
  import { resolveUserIdsQuery } from "./handlers/resolve-user-ids.query";
19
+ import { disableWrite, enableWrite } from "./handlers/toggle-enabled.write";
21
20
  import { updateWrite } from "./handlers/update.write";
22
21
  import { updateMemberRolesWrite } from "./handlers/update-member-roles.write";
23
22
  import { tenantInvitationEntity } from "./invitation-table";
@@ -0,0 +1,23 @@
1
+ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { z } from "zod";
4
+ import { tenantEntity, tenantTable } from "../schema/tenant";
5
+
6
+ const crud = createEventStoreExecutor(tenantTable, tenantEntity, { entityName: "tenant" });
7
+
8
+ // Admin flip: last-writer-wins is fine. SystemAdmin is the only caller and
9
+ // there's no meaningful concurrent-edit race on this single boolean.
10
+ function createToggleTenantHandler(enable: boolean) {
11
+ return defineWriteHandler({
12
+ name: enable ? "enable" : "disable",
13
+ schema: z.object({ id: z.uuid() }),
14
+ access: { roles: ["SystemAdmin"] },
15
+ handler: async (event, ctx) =>
16
+ crud.update({ id: event.payload.id, changes: { isEnabled: enable } }, event.user, ctx.db, {
17
+ skipOptimisticLock: true,
18
+ }),
19
+ });
20
+ }
21
+
22
+ export const enableWrite = createToggleTenantHandler(true);
23
+ export const disableWrite = createToggleTenantHandler(false);
@@ -90,15 +90,15 @@ weglassen wenn er Custom-Hooks registrieren will.
90
90
 
91
91
  | Datei | Pinst |
92
92
  |-------|-------|
93
- | `audit-log.integration.ts` | Cross-User-Isolation, Account-weite Sicht, eventType-Filter, Admin-only operator-query, download-attempt 90d-retention |
94
- | `cross-data-matrix.integration.ts` | 3-Provider-Pipeline (user + fileRef + custom-domain), Cross-Tenant Forget mit user-anonymize, Other-User-Isolation |
95
- | `download.integration.ts` | HTTP-e2e via `r.httpRoute`: Magic-Link, multi-use, expired, failed-job, storage-cleared, cross-tenant-same-user, malicious-filename |
96
- | `request-export.integration.ts` | Idempotency, active-job-constraint, cross-tenant-anyMember-userId-pattern |
97
- | `request-deletion-callback.integration.ts` + `request-cancel-deletion.integration.ts` | Grace + Cancel-Pfad + Email-Callback best-effort |
98
- | `restriction-flow.integration.ts` | Status-Flip + Auth-Middleware-Block + Lift |
99
- | `run-{export-jobs,forget-cleanup,user-export}.integration.ts` | Worker-Logic + Idempotency + Email-Callbacks |
93
+ | `audit-log.integration.test.ts` | Cross-User-Isolation, Account-weite Sicht, eventType-Filter, Admin-only operator-query, download-attempt 90d-retention |
94
+ | `cross-data-matrix.integration.test.ts` | 3-Provider-Pipeline (user + fileRef + custom-domain), Cross-Tenant Forget mit user-anonymize, Other-User-Isolation |
95
+ | `download.integration.test.ts` | HTTP-e2e via `r.httpRoute`: Magic-Link, multi-use, expired, failed-job, storage-cleared, cross-tenant-same-user, malicious-filename |
96
+ | `request-export.integration.test.ts` | Idempotency, active-job-constraint, cross-tenant-anyMember-userId-pattern |
97
+ | `request-deletion-callback.integration.test.ts` + `request-cancel-deletion.integration.test.ts` | Grace + Cancel-Pfad + Email-Callback best-effort |
98
+ | `restriction-flow.integration.test.ts` | Status-Flip + Auth-Middleware-Block + Lift |
99
+ | `run-{export-jobs,forget-cleanup,user-export}.integration.test.ts` | Worker-Logic + Idempotency + Email-Callbacks |
100
100
  | `policy-to-strategy.test.ts` | Retention.strategy → UserDataDeleteStrategy mapping |
101
- | `user-data-rights.integration.ts` | Boot-Smoke + Feature-Meta |
101
+ | `user-data-rights.integration.test.ts` | Boot-Smoke + Feature-Meta |
102
102
  | `token-helpers.test.ts` + `zip-path.test.ts` | Token-Hashing + Path-Traversal-Schutz |
103
103
  | `export-job-{idempotency,schema}.test.ts` | Active-job-uniqueness + Schema-Constraints |
104
104
 
@@ -1,39 +0,0 @@
1
- import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
- import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
- import { z } from "zod";
5
- import type { TemplateResourceRow } from "../table";
6
- import { templateResourcesTable } from "../table";
7
- import { executor } from "./shared";
8
-
9
- // Setzt einen Template-Eintrag auf status='archived'. Resolver liefert
10
- // archivierte Templates nicht zurück — sie bleiben aber als Audit-Trail
11
- // in der DB (kein physisches Delete). Reaktivierung via publish.
12
- export const archiveWrite = defineWriteHandler({
13
- name: "archive",
14
- schema: z.object({ id: z.string().min(1) }),
15
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
16
- handler: async (event, ctx) => {
17
- const existing = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
18
- id: event.payload.id,
19
- });
20
- // ctx.db ist via createTenantDb tenant-scoped — existing ist null wenn
21
- // das Template einem fremden Tenant gehört (SystemAdmin-Cross-Tenant
22
- // braucht tenantIdOverride im Schema, M2-Erweiterung).
23
- if (!existing) {
24
- return writeFailure(new NotFoundError("template-resource", event.payload.id));
25
- }
26
-
27
- const result = await executor.update(
28
- {
29
- id: existing.id,
30
- version: existing.version,
31
- changes: { status: "archived" as const },
32
- },
33
- event.user,
34
- ctx.db,
35
- );
36
- if (!result.isSuccess) return result;
37
- return { isSuccess: true as const, data: { id: String(existing.id), status: "archived" } };
38
- },
39
- });
@@ -1,42 +0,0 @@
1
- import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
- import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
- import { z } from "zod";
5
- import type { TemplateResourceRow } from "../table";
6
- import { templateResourcesTable } from "../table";
7
- import { executor } from "./shared";
8
-
9
- // Setzt einen Template-Eintrag auf status='active'. Typischer Workflow:
10
- // User editiert ein Draft, ist zufrieden, drückt Publish.
11
- //
12
- // Tenant-Isolation: Template muss zum event.user.tenantId gehören
13
- // (oder zu SYSTEM_TENANT wenn SystemAdmin). Cross-Tenant-Publish-
14
- // Versuche → NotFound (Pattern aus row-level-security).
15
- export const publishWrite = defineWriteHandler({
16
- name: "publish",
17
- schema: z.object({ id: z.string().min(1) }),
18
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
19
- handler: async (event, ctx) => {
20
- const existing = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
21
- id: event.payload.id,
22
- });
23
- // ctx.db ist via createTenantDb tenant-scoped — existing ist null wenn
24
- // das Template einem fremden Tenant gehört (SystemAdmin-Cross-Tenant
25
- // braucht tenantIdOverride im Schema, M2-Erweiterung).
26
- if (!existing) {
27
- return writeFailure(new NotFoundError("template-resource", event.payload.id));
28
- }
29
-
30
- const result = await executor.update(
31
- {
32
- id: existing.id,
33
- version: existing.version,
34
- changes: { status: "active" as const },
35
- },
36
- event.user,
37
- ctx.db,
38
- );
39
- if (!result.isSuccess) return result;
40
- return { isSuccess: true as const, data: { id: String(existing.id), status: "active" } };
41
- },
42
- });
@@ -1,18 +0,0 @@
1
- import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
- import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
- import { z } from "zod";
4
- import { tenantEntity, tenantTable } from "../schema/tenant";
5
-
6
- const crud = createEventStoreExecutor(tenantTable, tenantEntity, { entityName: "tenant" });
7
-
8
- export const disableWrite = defineWriteHandler({
9
- name: "disable",
10
- schema: z.object({ id: z.uuid() }),
11
- access: { roles: ["SystemAdmin"] },
12
- // Admin flip: last-writer-wins is fine. SystemAdmin is the only caller and
13
- // there's no meaningful concurrent-edit race on this single boolean.
14
- handler: async (event, ctx) =>
15
- crud.update({ id: event.payload.id, changes: { isEnabled: false } }, event.user, ctx.db, {
16
- skipOptimisticLock: true,
17
- }),
18
- });
@@ -1,20 +0,0 @@
1
- import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
- import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
- import { z } from "zod";
4
- import { tenantEntity, tenantTable } from "../schema/tenant";
5
-
6
- const crud = createEventStoreExecutor(tenantTable, tenantEntity, { entityName: "tenant" });
7
-
8
- // Recovery-Gegenstück zu disable — ohne enable wäre ein Fehlklick des
9
- // Operators nur per Event-Hack reversibel.
10
- export const enableWrite = defineWriteHandler({
11
- name: "enable",
12
- schema: z.object({ id: z.uuid() }),
13
- access: { roles: ["SystemAdmin"] },
14
- // Admin flip: last-writer-wins is fine. SystemAdmin is the only caller and
15
- // there's no meaningful concurrent-edit race on this single boolean.
16
- handler: async (event, ctx) =>
17
- crud.update({ id: event.payload.id, changes: { isEnabled: true } }, event.user, ctx.db, {
18
- skipOptimisticLock: true,
19
- }),
20
- });