@cosmicdrift/kumiko-bundled-features 0.2.2 → 0.2.3

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 (100) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/package.json +11 -5
  3. package/src/auth-email-password/auth-user-row.ts +6 -0
  4. package/src/auth-email-password/constants.ts +11 -0
  5. package/src/auth-email-password/handlers/login.write.ts +31 -1
  6. package/src/auth-email-password/i18n.ts +4 -0
  7. package/src/compliance-profiles/README.md +88 -0
  8. package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +308 -0
  9. package/src/compliance-profiles/__tests__/seeding.integration.ts +93 -0
  10. package/src/compliance-profiles/feature.ts +51 -0
  11. package/src/compliance-profiles/handlers/for-tenant.query.ts +63 -0
  12. package/src/compliance-profiles/handlers/list-profiles.query.ts +44 -0
  13. package/src/compliance-profiles/handlers/needs-profile.query.ts +56 -0
  14. package/src/compliance-profiles/handlers/set-profile.write.ts +146 -0
  15. package/src/compliance-profiles/handlers/sub-processors.query.ts +43 -0
  16. package/src/compliance-profiles/index.ts +6 -0
  17. package/src/compliance-profiles/resolve-for-tenant.ts +61 -0
  18. package/src/compliance-profiles/schema/profile-selection.ts +52 -0
  19. package/src/compliance-profiles/seeding.ts +96 -0
  20. package/src/data-retention/__tests__/data-retention.integration.ts +49 -0
  21. package/src/data-retention/__tests__/keep-for.test.ts +77 -0
  22. package/src/data-retention/__tests__/override-schema.test.ts +96 -0
  23. package/src/data-retention/__tests__/policy-for.integration.ts +172 -0
  24. package/src/data-retention/__tests__/resolver.test.ts +201 -0
  25. package/src/data-retention/_internal/parse-override.ts +33 -0
  26. package/src/data-retention/feature.ts +57 -0
  27. package/src/data-retention/handlers/policy-for.query.ts +57 -0
  28. package/src/data-retention/index.ts +18 -0
  29. package/src/data-retention/keep-for.ts +75 -0
  30. package/src/data-retention/override-schema.ts +37 -0
  31. package/src/data-retention/presets.ts +72 -0
  32. package/src/data-retention/resolve-for-tenant.ts +50 -0
  33. package/src/data-retention/resolver.ts +107 -0
  34. package/src/data-retention/schema/tenant-retention-override.ts +47 -0
  35. package/src/file-foundation/feature.ts +43 -3
  36. package/src/file-foundation/index.ts +1 -0
  37. package/src/file-provider-inmemory/feature.ts +6 -3
  38. package/src/file-provider-s3/feature.ts +8 -10
  39. package/src/files/README.md +50 -0
  40. package/src/files/__tests__/files.integration.ts +157 -0
  41. package/src/files/feature.ts +34 -0
  42. package/src/files/index.ts +1 -0
  43. package/src/files/schema/file-ref.ts +58 -0
  44. package/src/files-provider-s3/s3-provider.ts +89 -0
  45. package/src/secrets/__tests__/require-secrets-context.test.ts +81 -0
  46. package/src/secrets/feature.ts +10 -6
  47. package/src/sessions/constants.ts +4 -0
  48. package/src/sessions/feature.ts +3 -0
  49. package/src/sessions/handlers/revoke-all-for-user.write.ts +42 -0
  50. package/src/tier-engine/__tests__/auto-default-tier.integration.ts +118 -0
  51. package/src/tier-engine/feature.ts +16 -6
  52. package/src/user/__tests__/user-status.test.ts +39 -0
  53. package/src/user/index.ts +11 -1
  54. package/src/user/schema/user.ts +76 -0
  55. package/src/user-data-rights/COMPLIANCE.md +182 -0
  56. package/src/user-data-rights/README.md +109 -0
  57. package/src/user-data-rights/__tests__/audit-log.integration.ts +199 -0
  58. package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +349 -0
  59. package/src/user-data-rights/__tests__/download.integration.ts +565 -0
  60. package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +244 -0
  61. package/src/user-data-rights/__tests__/export-job-schema.test.ts +163 -0
  62. package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +30 -0
  63. package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +370 -0
  64. package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +179 -0
  65. package/src/user-data-rights/__tests__/request-export.integration.ts +269 -0
  66. package/src/user-data-rights/__tests__/restriction-flow.integration.ts +309 -0
  67. package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +1124 -0
  68. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +703 -0
  69. package/src/user-data-rights/__tests__/run-user-export.integration.ts +291 -0
  70. package/src/user-data-rights/__tests__/token-helpers.test.ts +63 -0
  71. package/src/user-data-rights/__tests__/user-data-rights.integration.ts +57 -0
  72. package/src/user-data-rights/__tests__/zip-path.test.ts +119 -0
  73. package/src/user-data-rights/audit-download.ts +125 -0
  74. package/src/user-data-rights/feature.ts +309 -0
  75. package/src/user-data-rights/handlers/cancel-deletion.write.ts +84 -0
  76. package/src/user-data-rights/handlers/download-by-job.query.ts +209 -0
  77. package/src/user-data-rights/handlers/download-by-token.query.ts +257 -0
  78. package/src/user-data-rights/handlers/export-status.query.ts +76 -0
  79. package/src/user-data-rights/handlers/lift-restriction.write.ts +68 -0
  80. package/src/user-data-rights/handlers/list-download-attempts.query.ts +53 -0
  81. package/src/user-data-rights/handlers/my-audit-log.query.ts +63 -0
  82. package/src/user-data-rights/handlers/request-deletion.write.ts +123 -0
  83. package/src/user-data-rights/handlers/request-export.write.ts +155 -0
  84. package/src/user-data-rights/handlers/restrict-account.write.ts +81 -0
  85. package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +61 -0
  86. package/src/user-data-rights/i18n.ts +37 -0
  87. package/src/user-data-rights/index.ts +19 -0
  88. package/src/user-data-rights/run-export-jobs.ts +878 -0
  89. package/src/user-data-rights/run-forget-cleanup.ts +334 -0
  90. package/src/user-data-rights/run-user-export.ts +211 -0
  91. package/src/user-data-rights/schema/download-attempt.ts +37 -0
  92. package/src/user-data-rights/schema/download-token.ts +111 -0
  93. package/src/user-data-rights/schema/export-job.ts +166 -0
  94. package/src/user-data-rights/token-helpers.ts +67 -0
  95. package/src/user-data-rights/zip-path.ts +94 -0
  96. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +337 -0
  97. package/src/user-data-rights-defaults/feature.ts +40 -0
  98. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +109 -0
  99. package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +91 -0
  100. package/src/user-data-rights-defaults/index.ts +6 -0
@@ -0,0 +1,155 @@
1
+ // POST /api/user/request-export (S2.U3 Atom 2) — DSGVO Art. 15 + 20 Trigger.
2
+ //
3
+ // User triggert Export-Job. Persistenz via createEventStoreExecutor
4
+ // (ES-Pattern, Memory: ES kein CRUD). Idempotency-Strategie zweistufig:
5
+ //
6
+ // 1) **App-side-Pre-Check (primaerer Pfad):** fetchOne aktive Jobs
7
+ // (status pending|running) fuer den userId. Wenn existing,
8
+ // return `{jobId, isExisting: true}` — KEIN neues Event,
9
+ // sauberer Audit-Trail (1 Klick-Storm = 1 Event, nicht N).
10
+ //
11
+ // 2) **DB-Constraint als Race-Schutz (Sekundaerpfad):** Bei parallelem
12
+ // Klick zwischen fetchOne + crud.create wirft die Tx 23505 mit
13
+ // constraint_name === "read_export_jobs_one_active_per_user".
14
+ // Catch + re-fetch + return existing als isExisting=true. Race-
15
+ // Window <1ms, in Production extrem selten.
16
+ //
17
+ // **Cross-Tenant-Semantik:** ExportJob ist tenant-agnostisch (1 Job pro
18
+ // userId ueber alle Memberships). Pre-Check nutzt ctx.db.raw (kein
19
+ // TenantDb-Filter) — Alice klickt aus Tenant A, klickt dann aus Tenant
20
+ // B → Pre-Check aus B findet den A-Job, kein 2. Job entsteht.
21
+ // `requestedFromTenantId` persistiert den Initial-Tenant aus dem
22
+ // 1. Klick — Worker liest sein Compliance-Profile aus DIESEM Tenant
23
+ // fuer Job-TTL/Stale/Cleanup.
24
+
25
+ import { createEventStoreExecutor, fetchOne } from "@cosmicdrift/kumiko-framework/db";
26
+ import { defineWriteHandler, type SaveContext } from "@cosmicdrift/kumiko-framework/engine";
27
+ import type { WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
28
+ import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
29
+ import { eq, inArray } from "drizzle-orm";
30
+ import { z } from "zod";
31
+ import {
32
+ ACTIVE_JOB_CONSTRAINT,
33
+ EXPORT_JOB_STATUS,
34
+ exportJobEntity,
35
+ exportJobsTable,
36
+ } from "../schema/export-job";
37
+
38
+ const crud = createEventStoreExecutor(exportJobsTable, exportJobEntity, {
39
+ entityName: "export-job",
40
+ });
41
+
42
+ /**
43
+ * Race-Loss-Detection: createEventStoreExecutor.create catched 23505
44
+ * intern + returnt `WriteFailure(UniqueViolationError)`, **kein throw**.
45
+ * Wir checken den Failure-Code + constraintName um den App-side-vs-Race-
46
+ * Pfad zu unterscheiden. Andere Failures (validation, version-conflict,
47
+ * ...) propagieren unveraendert.
48
+ */
49
+ function isActiveJobConflict(failure: WriteFailure): boolean {
50
+ const error = failure.error as {
51
+ code?: string;
52
+ details?: { constraintName?: string };
53
+ };
54
+ return (
55
+ error.code === "unique_violation" && error.details?.constraintName === ACTIVE_JOB_CONSTRAINT
56
+ );
57
+ }
58
+
59
+ export const requestExportWrite = defineWriteHandler({
60
+ name: "request-export",
61
+ schema: z.object({}),
62
+ access: { openToAll: true },
63
+ handler: async (event, ctx) => {
64
+ const userId = event.user.id;
65
+ const T = getTemporal();
66
+ const now = T.Now.instant();
67
+
68
+ // Pre-Check: ctx.db.raw weil ExportJob tenant-agnostisch ist —
69
+ // der TenantDb-Wrapper wuerde Cross-Tenant-Jobs ausblenden.
70
+ const existing = await findActiveJob(ctx.db.raw, userId);
71
+ if (existing) {
72
+ // Snapshot-Status (kann zwischen fetchOne + Response stale werden
73
+ // wenn Worker parallel den State flippt; window minimal). User
74
+ // pollt fuer Live-Wahrheit ueber export-status.query.
75
+ return {
76
+ isSuccess: true as const,
77
+ data: {
78
+ jobId: existing.id,
79
+ status: existing.status,
80
+ isExisting: true,
81
+ },
82
+ };
83
+ }
84
+
85
+ // Kein active Job — neuen anlegen via crud.create. requestedFromTenantId
86
+ // = aktueller Tenant des Users; Worker liest sein Compliance-Profile
87
+ // aus diesem Tenant.
88
+ const result = await crud.create(
89
+ {
90
+ userId,
91
+ requestedFromTenantId: event.user.tenantId,
92
+ requestedAt: now,
93
+ tenantId: event.user.tenantId,
94
+ },
95
+ event.user,
96
+ ctx.db,
97
+ );
98
+
99
+ if (!result.isSuccess) {
100
+ // Race-Pfad: paralleler Klick hat zwischen findActiveJob + crud.create
101
+ // einen aktiven Job angelegt → UniqueViolationError mit unserem
102
+ // Constraint. Re-fetch + return existing als isExisting=true.
103
+ // Andere Failures (validation, version-conflict, ...) propagieren.
104
+ if (isActiveJobConflict(result)) {
105
+ const winner = await findActiveJob(ctx.db.raw, userId);
106
+ if (!winner) {
107
+ // Sollte nie passieren — Constraint-Violation ohne existing
108
+ // active Job hiesse der Constraint matcht etwas anderes.
109
+ // Original-Failure zurueckgeben damit das auffaellt.
110
+ return result;
111
+ }
112
+ return {
113
+ isSuccess: true as const,
114
+ data: {
115
+ jobId: winner.id,
116
+ status: winner.status,
117
+ isExisting: true,
118
+ },
119
+ };
120
+ }
121
+ return result;
122
+ }
123
+
124
+ // Happy path: neuer Job. SaveContext.id ist EntityId (number | string);
125
+ // exportJobEntity hat idType:"uuid" → garantiert string, String()
126
+ // schuetzt vor Drift falls jemand die Entity auf serial migriert.
127
+ const created = result.data as SaveContext;
128
+ return {
129
+ isSuccess: true as const,
130
+ data: {
131
+ jobId: String(created.id),
132
+ // Snapshot: just-created → pending. Live-Wahrheit kommt ueber
133
+ // export-status.query, status hier ist nice-to-have fuer das
134
+ // initial UI-Render ("Anfrage angenommen, currently pending").
135
+ status: EXPORT_JOB_STATUS.Pending,
136
+ isExisting: false,
137
+ },
138
+ };
139
+ },
140
+ });
141
+
142
+ async function findActiveJob(
143
+ db: import("@cosmicdrift/kumiko-framework/db").DbRunner,
144
+ userId: string,
145
+ ): Promise<{ id: string; status: string } | null> {
146
+ // @cast-boundary db-row — fetchOne liefert generic DbRow.
147
+ // Variadic-conditions werden intern mit AND verknuepft.
148
+ const row = (await fetchOne(
149
+ db,
150
+ exportJobsTable,
151
+ eq(exportJobsTable["userId"], userId),
152
+ inArray(exportJobsTable["status"], [EXPORT_JOB_STATUS.Pending, EXPORT_JOB_STATUS.Running]),
153
+ )) as { id: string; status: string } | null;
154
+ return row;
155
+ }
@@ -0,0 +1,81 @@
1
+ import { createSystemUser, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+ import { USER_STATUS, userTable } from "../../user";
6
+
7
+ // POST /api/user/restrict (S2.U6) — DSGVO Art. 18 Account-Freeze.
8
+ // Flippt status=Active → Restricted und revoked alle live sessions
9
+ // des Users via cross-feature ctx.writeAs(sessions.revokeAllForUser).
10
+ //
11
+ // Plan-Doc-Verhalten ("Schreib-API geblockt"):
12
+ // - Login geblockt: login.write.ts checked status=Restricted (Atom 3).
13
+ // - Active sessions: revoked durch cross-feature-call (sessions-feature
14
+ // muss gemountet sein). App-Author ohne sessions-feature kriegt einen
15
+ // Boot-Resolver-Error via r.usesApi("sessions.revokeAllForUser").
16
+ //
17
+ // State-Transitions:
18
+ // Active → Restricted ✓ (dieser Handler)
19
+ // Restricted → Restricted ✗ 422 already_restricted (Idempotenz-Guard)
20
+ // DeletionRequested → ... ✗ 422 user_not_in_active_state
21
+ // Deleted → ... ✗ 422 user_not_in_active_state
22
+ export const restrictAccountWrite = defineWriteHandler({
23
+ name: "restrict-account",
24
+ schema: z.object({}),
25
+ access: { openToAll: true },
26
+ handler: async (event, ctx) => {
27
+ // ctx.db.raw weil User-Entity tenant-agnostisch ist (analog
28
+ // request-deletion.write.ts Cross-Tenant-Section).
29
+ const userRow = await ctx.db.raw
30
+ .select({ status: userTable["status"] })
31
+ .from(userTable)
32
+ .where(eq(userTable["id"], event.user.id))
33
+ .limit(1);
34
+
35
+ if (userRow.length === 0) {
36
+ return writeFailure(
37
+ new UnprocessableError("user_not_found", {
38
+ details: { reason: "user_not_found", userId: event.user.id },
39
+ }),
40
+ );
41
+ }
42
+
43
+ const currentStatus = userRow[0]?.status;
44
+ if (currentStatus === USER_STATUS.Restricted) {
45
+ return writeFailure(
46
+ new UnprocessableError("already_restricted", {
47
+ details: { reason: "already_restricted", currentStatus },
48
+ }),
49
+ );
50
+ }
51
+ if (currentStatus !== USER_STATUS.Active) {
52
+ return writeFailure(
53
+ new UnprocessableError("user_not_in_active_state", {
54
+ details: { reason: "user_not_in_active_state", currentStatus },
55
+ }),
56
+ );
57
+ }
58
+
59
+ await ctx.db.raw
60
+ .update(userTable)
61
+ .set({ status: USER_STATUS.Restricted })
62
+ .where(eq(userTable["id"], event.user.id));
63
+
64
+ // Cross-Feature: alle live sessions revoken — sonst koennte der User
65
+ // mit existierendem JWT bis zur Token-Expiry weiter schreiben.
66
+ // ctx.writeAs(systemUser, ...) damit der privileged-Handler die
67
+ // System-User-Roles im access-gate hat.
68
+ const systemUser = createSystemUser(event.user.tenantId);
69
+ await ctx.writeAs(systemUser, "sessions:write:user-session:revoke-all-for-user", {
70
+ userId: event.user.id,
71
+ });
72
+
73
+ return {
74
+ isSuccess: true as const,
75
+ data: {
76
+ userId: event.user.id,
77
+ status: USER_STATUS.Restricted,
78
+ },
79
+ };
80
+ },
81
+ });
@@ -0,0 +1,61 @@
1
+ // POST /userDataRights/run-forget-cleanup (S2.U5b).
2
+ //
3
+ // Cron-Trigger fuer den Forget-Cleanup-Pipeline. Der Handler wrapt nur
4
+ // `runForgetCleanup` damit er via dispatcher (System-User) aufrufbar
5
+ // wird — die Pipeline-Logik selbst lebt im pure-function Modul
6
+ // `run-forget-cleanup.ts` (testbar ohne dispatcher).
7
+ //
8
+ // Access: privileged — nur System-Caller (cron, ops-script). Im Cron-
9
+ // Setup laeuft der Job mit `createSystemUser(...)` als executor.
10
+ //
11
+ // Rueckgabe: Stats fuer Operator-Monitoring (processed-count, error-list).
12
+
13
+ import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
14
+ import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
15
+ import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
16
+ import { z } from "zod";
17
+ import { runForgetCleanup, type SendDeletionExecutedEmailFn } from "../run-forget-cleanup";
18
+
19
+ export type RunForgetCleanupOptions = {
20
+ readonly sendDeletionExecutedEmail?: SendDeletionExecutedEmailFn;
21
+ };
22
+
23
+ export function createRunForgetCleanupHandler(opts: RunForgetCleanupOptions = {}) {
24
+ return defineWriteHandler({
25
+ name: "run-forget-cleanup",
26
+ schema: z.object({}),
27
+ access: { roles: access.privileged },
28
+ handler: async (_event, ctx) => {
29
+ if (!ctx.registry) {
30
+ return writeFailure(
31
+ new InternalError({
32
+ message: "run-forget-cleanup: ctx.registry missing",
33
+ }),
34
+ );
35
+ }
36
+
37
+ // ctx.db.raw ist DbRunner. runForgetCleanup oeffnet pro User eine
38
+ // Sub-Tx (SAVEPOINT wenn Outer-Dispatcher-Tx aktiv) — siehe
39
+ // run-forget-cleanup.ts Header. Kein DbConnection-Cast noetig.
40
+ const T = getTemporal();
41
+ const result = await runForgetCleanup({
42
+ db: ctx.db.raw,
43
+ registry: ctx.registry,
44
+ now: T.Now.instant(),
45
+ ...(opts.sendDeletionExecutedEmail && {
46
+ sendDeletionExecutedEmail: opts.sendDeletionExecutedEmail,
47
+ }),
48
+ });
49
+
50
+ return {
51
+ isSuccess: true as const,
52
+ data: {
53
+ processedUserIds: result.processedUserIds,
54
+ hookCallsAttempted: result.hookCallsAttempted,
55
+ errorCount: result.errors.length,
56
+ errors: result.errors,
57
+ },
58
+ };
59
+ },
60
+ });
61
+ }
@@ -0,0 +1,37 @@
1
+ // @runtime client
2
+ //
3
+ // Default-Translations fuer user-data-rights Error-Wire-Keys (S2.U3 Atom 4b.fix3).
4
+ //
5
+ // Wire-Errors aus den download-handlers tragen i18nKeys statt fixe
6
+ // Strings — UI rendert die Keys ueber den Renderer-LocaleProvider. Apps
7
+ // koennen einzelne Keys via `userDataRightsClient({ translations: { de:
8
+ // {...} } })` ueberschreiben (Pattern matched auth-email-password).
9
+ //
10
+ // **Scope 4b.fix3:** nur die download-error-keys. Andere Keys (UI-
11
+ // Texte fuer Forget-Pfad, Status-Labels, Banner) kommen mit Atom 6+
12
+ // (UI-Integration).
13
+
14
+ import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
15
+
16
+ export const defaultTranslations: TranslationsByLocale = {
17
+ de: {
18
+ "userDataRights.errors.download.notFound":
19
+ "Der Download-Link ist ungültig oder gehört zu einem anderen Konto.",
20
+ "userDataRights.errors.download.expired":
21
+ "Dein Download ist abgelaufen. Bitte fordere einen neuen Export an.",
22
+ "userDataRights.errors.download.unavailable":
23
+ "Der Export ist noch nicht fertig oder fehlgeschlagen. Bitte schau im Status-Polling nach.",
24
+ "userDataRights.errors.download.signedUrlNotSupported":
25
+ "Der Download steht aufgrund einer Server-Konfiguration aktuell nicht zur Verfügung. Der Operator wurde benachrichtigt.",
26
+ },
27
+ en: {
28
+ "userDataRights.errors.download.notFound":
29
+ "The download link is invalid or belongs to a different account.",
30
+ "userDataRights.errors.download.expired":
31
+ "Your download has expired. Please request a new export.",
32
+ "userDataRights.errors.download.unavailable":
33
+ "The export is not yet ready or has failed. Please check the status endpoint.",
34
+ "userDataRights.errors.download.signedUrlNotSupported":
35
+ "The download is currently unavailable due to a server configuration issue. The operator has been notified.",
36
+ },
37
+ };
@@ -0,0 +1,19 @@
1
+ export { createUserDataRightsFeature, type UserDataRightsOptions } from "./feature";
2
+ export type {
3
+ SendExportFailedEmailFn,
4
+ SendExportReadyEmailFn,
5
+ } from "./run-export-jobs";
6
+ export type { SendDeletionExecutedEmailFn } from "./run-forget-cleanup";
7
+ // Runner-Exports — App-Tests dürfen export/forget deterministisch laufen
8
+ // lassen, statt über den Job-Cron zu warten (siehe sample
9
+ // user-data-rights-demo).
10
+ export { runForgetCleanup } from "./run-forget-cleanup";
11
+ export type { UserExportBundle } from "./run-user-export";
12
+ export { runUserExport } from "./run-user-export";
13
+ export {
14
+ ACTIVE_JOB_CONSTRAINT,
15
+ EXPORT_JOB_STATUS,
16
+ type ExportJobStatus,
17
+ exportJobEntity,
18
+ exportJobsTable,
19
+ } from "./schema/export-job";