@cosmicdrift/kumiko-bundled-features 0.79.0 → 0.79.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.79.0",
3
+ "version": "0.79.2",
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>",
@@ -84,11 +84,11 @@
84
84
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
85
85
  },
86
86
  "dependencies": {
87
- "@cosmicdrift/kumiko-dispatcher-live": "0.79.0",
88
- "@cosmicdrift/kumiko-framework": "0.79.0",
89
- "@cosmicdrift/kumiko-headless": "0.79.0",
90
- "@cosmicdrift/kumiko-renderer": "0.79.0",
91
- "@cosmicdrift/kumiko-renderer-web": "0.79.0",
87
+ "@cosmicdrift/kumiko-dispatcher-live": "0.79.2",
88
+ "@cosmicdrift/kumiko-framework": "0.79.2",
89
+ "@cosmicdrift/kumiko-headless": "0.79.2",
90
+ "@cosmicdrift/kumiko-renderer": "0.79.2",
91
+ "@cosmicdrift/kumiko-renderer-web": "0.79.2",
92
92
  "@mollie/api-client": "^4.5.0",
93
93
  "@node-rs/argon2": "^2.0.2",
94
94
  "@types/nodemailer": "^8.0.0",
@@ -0,0 +1,148 @@
1
+ // Treibt den ECHTEN registrierten Export-Cron-Job (r.job "run-export-jobs")
2
+ // über seinen Job-Kontext — so wie der Job-Runner ihn in prod aufruft:
3
+ // `ctx.configResolver` gesetzt (App-Override provider=inmemory), aber KEIN
4
+ // per-request `ctx.config` (das baut nur der HTTP-Dispatcher).
5
+ //
6
+ // Der bestehende run-export-jobs-Test reicht `buildStorageProvider` MANUELL —
7
+ // und übersprang damit genau diesen Pfad: der Wrapper baut providerCtx aus dem
8
+ // Job-Kontext. Ohne den configResolver→ConfigAccessor-Bau wirft
9
+ // createFileProviderForTenant "ctx.config is missing" (genau der prod-Bug, der
10
+ // jeden Export auf "failed" setzte).
11
+
12
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
13
+ import { asRawClient, insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
14
+ import { SYSTEM_USER_ID } from "@cosmicdrift/kumiko-framework/engine";
15
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
16
+ import {
17
+ setupTestStack,
18
+ type TestStack,
19
+ unsafeCreateEntityTable,
20
+ unsafePushTables,
21
+ } from "@cosmicdrift/kumiko-framework/stack";
22
+ import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
23
+ import {
24
+ createComplianceProfilesFeature,
25
+ tenantComplianceProfileEntity,
26
+ } from "../../compliance-profiles";
27
+ import { configValuesTable, createConfigFeature, createConfigResolver } from "../../config";
28
+ import { createDataRetentionFeature } from "../../data-retention";
29
+ import { fileFoundationFeature } from "../../file-foundation";
30
+ import { fileProviderInMemoryFeature } from "../../file-provider-inmemory";
31
+ import { createSessionsFeature } from "../../sessions";
32
+ import { createUserFeature, USER_STATUS, userEntity, userTable } from "../../user";
33
+ import { createUserDataRightsFeature } from "../feature";
34
+ import { exportDownloadTokenEntity } from "../schema/download-token";
35
+ import { EXPORT_JOB_STATUS, exportJobEntity, exportJobsTable } from "../schema/export-job";
36
+
37
+ const TENANT = "00000000-0000-4000-8000-0000000009a1";
38
+ const USER_ID = "00000000-0000-4000-8000-0000000009b1";
39
+ const JOB_QN = "user-data-rights:job:run-export-jobs";
40
+
41
+ // App-weiter Override wie money-horse's cashColtConfigResolver — provider=inmemory
42
+ // ohne per-Tenant-config-Row. Der Job-Kontext trägt DIESEN resolver, kein config.
43
+ const configResolver = createConfigResolver({
44
+ appOverrides: new Map([["file-foundation:config:provider", "inmemory"]]),
45
+ });
46
+
47
+ let stack: TestStack;
48
+
49
+ beforeAll(async () => {
50
+ stack = await setupTestStack({
51
+ features: [
52
+ createConfigFeature(),
53
+ createUserFeature(),
54
+ createDataRetentionFeature(),
55
+ createComplianceProfilesFeature(),
56
+ fileFoundationFeature,
57
+ fileProviderInMemoryFeature,
58
+ createSessionsFeature(),
59
+ createUserDataRightsFeature(),
60
+ ],
61
+ });
62
+ await createEventsTable(stack.db);
63
+ await unsafePushTables(stack.db, { configValuesTable });
64
+ await unsafeCreateEntityTable(stack.db, exportJobEntity);
65
+ await unsafeCreateEntityTable(stack.db, exportDownloadTokenEntity);
66
+ await unsafeCreateEntityTable(stack.db, tenantComplianceProfileEntity);
67
+ await unsafeCreateEntityTable(stack.db, userEntity);
68
+ await asRawClient(stack.db).unsafe(`
69
+ CREATE TABLE IF NOT EXISTS read_tenant_memberships (
70
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
71
+ tenant_id UUID NOT NULL,
72
+ user_id TEXT NOT NULL,
73
+ version INTEGER NOT NULL DEFAULT 0,
74
+ inserted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
75
+ modified_at TIMESTAMPTZ NOT NULL DEFAULT now(),
76
+ inserted_by_id TEXT,
77
+ modified_by_id TEXT,
78
+ is_deleted BOOLEAN NOT NULL DEFAULT false,
79
+ deleted_at TIMESTAMPTZ,
80
+ deleted_by_id TEXT,
81
+ roles TEXT NOT NULL DEFAULT '[]',
82
+ UNIQUE(user_id, tenant_id)
83
+ )
84
+ `);
85
+ });
86
+
87
+ afterAll(async () => {
88
+ await stack.cleanup();
89
+ });
90
+
91
+ beforeEach(async () => {
92
+ const raw = asRawClient(stack.db);
93
+ await raw.unsafe("DELETE FROM read_export_jobs");
94
+ await raw.unsafe("DELETE FROM read_users");
95
+ await raw.unsafe("DELETE FROM read_tenant_memberships");
96
+ await insertOne(stack.db, userTable, {
97
+ id: USER_ID,
98
+ tenantId: TENANT,
99
+ email: "export-cron@example.test",
100
+ passwordHash: "hashed",
101
+ displayName: "Cron Export",
102
+ locale: "de",
103
+ emailVerified: true,
104
+ roles: '["Member"]',
105
+ status: USER_STATUS.Active,
106
+ });
107
+ await raw.unsafe(
108
+ `INSERT INTO read_tenant_memberships (tenant_id, user_id, roles) VALUES ('${TENANT}', '${USER_ID}', '["Member"]')`,
109
+ );
110
+ });
111
+
112
+ // Seedet einen pending Export-Job über den echten request-export-Handler.
113
+ async function seedPendingJob(): Promise<string> {
114
+ const res = await stack.http.writeOk<{ jobId: string }>(
115
+ "user-data-rights:write:request-export",
116
+ {},
117
+ { id: USER_ID, tenantId: TENANT, roles: ["Member"] },
118
+ );
119
+ return res.jobId;
120
+ }
121
+
122
+ describe("run-export-jobs cron-context", () => {
123
+ test("Cron-Job-Kontext (configResolver, KEIN config) → Export läuft durch, bytesWritten > 0", async () => {
124
+ const jobId = await seedPendingJob();
125
+ const job = stack.registry.getJob(JOB_QN);
126
+ expect(job).toBeDefined();
127
+
128
+ // EXAKT der prod-Job-Kontext: configResolver gesetzt, config undefined.
129
+ const jobCtx = {
130
+ db: stack.db,
131
+ registry: stack.registry,
132
+ configResolver,
133
+ _userId: SYSTEM_USER_ID,
134
+ now: getTemporal().Now.instant(),
135
+ };
136
+ // Vor dem Fix wirft der Wrapper hier "ctx.config is missing".
137
+ await job?.handler({}, jobCtx as never);
138
+
139
+ const [row] = (await selectMany(stack.db, exportJobsTable, { id: jobId })) as Array<{
140
+ status: string;
141
+ bytesWritten: number | null;
142
+ errorMessage: string | null;
143
+ }>;
144
+ expect(row?.errorMessage).toBeNull();
145
+ expect(row?.status).toBe(EXPORT_JOB_STATUS.Done);
146
+ expect(row?.bytesWritten ?? 0).toBeGreaterThan(0);
147
+ });
148
+ });
@@ -4,6 +4,7 @@ import {
4
4
  type FeatureDefinition,
5
5
  SYSTEM_USER_ID,
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { createConfigAccessor } from "../config";
7
8
  import { createFileProviderForTenant } from "../file-foundation";
8
9
  import { PRIVACY_CENTER_SCREEN_ID } from "./constants";
9
10
  import { cancelDeletionWrite } from "./handlers/cancel-deletion.write";
@@ -295,17 +296,35 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
295
296
  // SYSTEM_USER_ID ist die framework-weite Konvention. Der job-
296
297
  // Discriminator wird via handlerName="user-data-rights:run-export-
297
298
  // jobs" im Secret-Read-Audit erfasst.
298
- const providerCtx = {
299
- config: ctx.config,
300
- registry: ctx.registry,
301
- secrets: ctx.secrets,
302
- _userId: ctx._userId ?? SYSTEM_USER_ID,
303
- };
299
+ const exportUserId = ctx._userId ?? SYSTEM_USER_ID;
300
+ const exportDb = ctx.db as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator
301
+ const exportRegistry = ctx.registry;
304
302
  await runExportJobs({
305
- db: ctx.db as import("@cosmicdrift/kumiko-framework/db").DbConnection, // @cast-boundary db-operator
306
- registry: ctx.registry,
307
- buildStorageProvider: async (tenantId) =>
308
- createFileProviderForTenant(providerCtx, tenantId, "user-data-rights:run-export-jobs"), // @wrapper-known semantic-alias
303
+ db: exportDb,
304
+ registry: exportRegistry,
305
+ buildStorageProvider: async (tenantId) => {
306
+ // ctx.config (per-request ConfigAccessor) existiert nur im HTTP-
307
+ // Dispatcher; der Cron-Job-Kontext trägt ctx.configResolver. Den
308
+ // per-Tenant-Accessor daraus bauen (wie der HTTP-Pfad via
309
+ // _configAccessorFactory) — sonst wirft createFileProviderForTenant
310
+ // "ctx.config is missing" und jeder Export landet auf failed.
311
+ const config =
312
+ ctx.config ??
313
+ (ctx.configResolver
314
+ ? createConfigAccessor(
315
+ exportRegistry,
316
+ ctx.configResolver,
317
+ tenantId as Parameters<typeof createConfigAccessor>[2],
318
+ exportUserId,
319
+ exportDb,
320
+ )
321
+ : undefined);
322
+ return createFileProviderForTenant(
323
+ { config, registry: exportRegistry, secrets: ctx.secrets, _userId: exportUserId },
324
+ tenantId,
325
+ "user-data-rights:run-export-jobs",
326
+ );
327
+ },
309
328
  now: T.Now.instant(),
310
329
  // Atom 5 — App-Author-Callbacks fuer Email-Notification.
311
330
  // Optional: wenn nicht gesetzt, kein Email; User pollt