@cosmicdrift/kumiko-bundled-features 0.2.3 → 0.4.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 (127) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/package.json +19 -14
  3. package/src/auth-email-password/handlers/change-password.write.ts +1 -1
  4. package/src/auth-email-password/handlers/confirm-token-flow.ts +1 -1
  5. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +7 -7
  6. package/src/auth-email-password/handlers/invite-accept.write.ts +7 -6
  7. package/src/auth-email-password/handlers/invite-create.write.ts +3 -3
  8. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -4
  9. package/src/auth-email-password/handlers/login.write.ts +1 -1
  10. package/src/auth-email-password/handlers/logout.write.ts +2 -2
  11. package/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
  12. package/src/auth-email-password/web/auth-client.ts +1 -1
  13. package/src/billing-foundation/events.ts +1 -1
  14. package/src/billing-foundation/feature.ts +44 -47
  15. package/src/billing-foundation/handlers/create-portal-session.write.ts +3 -3
  16. package/src/billing-foundation/handlers/process-event.write.ts +3 -3
  17. package/src/billing-foundation/projection.ts +1 -1
  18. package/src/billing-foundation/webhook-handler.ts +1 -1
  19. package/src/cap-counter/constants.ts +1 -1
  20. package/src/cap-counter/enforce-cap.ts +1 -1
  21. package/src/cap-counter/feature.ts +3 -7
  22. package/src/cap-counter/handlers/get-counter.query.ts +1 -1
  23. package/src/cap-counter/handlers/increment-rolling.write.ts +2 -2
  24. package/src/cap-counter/handlers/increment.write.ts +3 -3
  25. package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
  26. package/src/channel-email/email-channel.ts +1 -1
  27. package/src/channel-email/types.ts +1 -1
  28. package/src/compliance-profiles/handlers/for-tenant.query.ts +7 -6
  29. package/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
  30. package/src/compliance-profiles/handlers/set-profile.write.ts +6 -8
  31. package/src/compliance-profiles/resolve-for-tenant.ts +7 -5
  32. package/src/compliance-profiles/seeding.ts +1 -1
  33. package/src/config/resolver.ts +1 -1
  34. package/src/data-retention/_internal/parse-override.ts +3 -2
  35. package/src/data-retention/handlers/policy-for.query.ts +1 -1
  36. package/src/data-retention/keep-for.ts +1 -1
  37. package/src/data-retention/presets.ts +1 -1
  38. package/src/data-retention/resolve-for-tenant.ts +1 -1
  39. package/src/delivery/__tests__/delivery.integration.ts +6 -0
  40. package/src/delivery/delivery-service.ts +4 -12
  41. package/src/delivery/feature.ts +7 -5
  42. package/src/delivery/index.ts +0 -1
  43. package/src/delivery/testing.ts +1 -2
  44. package/src/delivery/upsert-preference.ts +1 -1
  45. package/src/feature-toggles/feature.ts +1 -1
  46. package/src/feature-toggles/handlers/list.query.ts +1 -1
  47. package/src/feature-toggles/handlers/registered.query.ts +9 -2
  48. package/src/feature-toggles/handlers/set.write.ts +3 -3
  49. package/src/file-foundation/feature.ts +1 -1
  50. package/src/file-provider-s3/feature.ts +2 -2
  51. package/src/files-provider-s3/s3-provider.ts +2 -2
  52. package/src/jobs/handlers/list.query.ts +3 -3
  53. package/src/jobs/handlers/trigger.write.ts +1 -1
  54. package/src/legal-pages/constants.ts +1 -0
  55. package/src/legal-pages/web/client-plugin.ts +82 -0
  56. package/src/legal-pages/web/index.ts +4 -0
  57. package/src/mail-foundation/feature.ts +1 -1
  58. package/src/mail-transport-smtp/feature.ts +2 -2
  59. package/src/renderer-foundation/README.md +86 -0
  60. package/src/renderer-foundation/__tests__/api.test.ts +188 -0
  61. package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +101 -0
  62. package/src/renderer-foundation/api.ts +106 -0
  63. package/src/renderer-foundation/constants.ts +21 -0
  64. package/src/renderer-foundation/feature.ts +47 -0
  65. package/src/renderer-foundation/index.ts +25 -0
  66. package/src/renderer-foundation/types.ts +109 -0
  67. package/src/renderer-simple/__tests__/adapter.test.ts +50 -0
  68. package/src/renderer-simple/feature.ts +28 -3
  69. package/src/renderer-simple/simple-renderer.ts +1 -1
  70. package/src/secrets/handlers/rotate.job.ts +2 -2
  71. package/src/sessions/handlers/cleanup.job.ts +2 -2
  72. package/src/step-dispatcher/feature.ts +62 -0
  73. package/src/step-dispatcher/index.ts +16 -0
  74. package/src/step-dispatcher/mail-runner.ts +32 -0
  75. package/src/step-dispatcher/webhook-runner.ts +67 -0
  76. package/src/subscription-mollie/plugin-methods.ts +1 -1
  77. package/src/subscription-mollie/verify-webhook.ts +9 -5
  78. package/src/subscription-stripe/verify-webhook.ts +3 -3
  79. package/src/template-resolver/README.md +89 -0
  80. package/src/template-resolver/__tests__/handlers.integration.ts +403 -0
  81. package/src/template-resolver/__tests__/template-resolver.integration.ts +570 -0
  82. package/src/template-resolver/api.ts +189 -0
  83. package/src/template-resolver/constants.ts +28 -0
  84. package/src/template-resolver/feature.ts +36 -0
  85. package/src/template-resolver/handlers/archive.write.ts +42 -0
  86. package/src/template-resolver/handlers/find-by-id.query.ts +45 -0
  87. package/src/template-resolver/handlers/list.query.ts +69 -0
  88. package/src/template-resolver/handlers/publish.write.ts +45 -0
  89. package/src/template-resolver/handlers/shared.ts +41 -0
  90. package/src/template-resolver/handlers/upsert-system.write.ts +75 -0
  91. package/src/template-resolver/handlers/upsert-tenant.write.ts +98 -0
  92. package/src/template-resolver/index.ts +28 -0
  93. package/src/template-resolver/qualified-names.ts +24 -0
  94. package/src/template-resolver/table.ts +67 -0
  95. package/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
  96. package/src/tenant/handlers/cancel-invitation.write.ts +1 -1
  97. package/src/tenant/handlers/remove-member.write.ts +1 -1
  98. package/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
  99. package/src/tenant/handlers/update-member-roles.write.ts +3 -3
  100. package/src/text-content/__tests__/text-content.integration.ts +54 -0
  101. package/src/text-content/constants.ts +2 -0
  102. package/src/text-content/feature.ts +20 -4
  103. package/src/text-content/handlers/by-slug.query.ts +1 -0
  104. package/src/text-content/handlers/by-tenant.query.ts +58 -0
  105. package/src/text-content/handlers/set.write.ts +24 -1
  106. package/src/text-content/seeding.ts +9 -1
  107. package/src/text-content/table.ts +6 -0
  108. package/src/text-content/web/__tests__/editor-read-only.test.tsx +125 -0
  109. package/src/text-content/web/__tests__/group-blocks.test.ts +221 -0
  110. package/src/text-content/web/client-plugin.tsx +378 -0
  111. package/src/text-content/web/index.ts +8 -0
  112. package/src/tier-engine/feature.ts +8 -8
  113. package/src/user/handlers/find-for-auth.query.ts +1 -1
  114. package/src/user/seeding.ts +2 -2
  115. package/src/user-data-rights/feature.ts +4 -3
  116. package/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
  117. package/src/user-data-rights/handlers/download-by-job.query.ts +8 -11
  118. package/src/user-data-rights/handlers/download-by-token.query.ts +14 -16
  119. package/src/user-data-rights/handlers/export-status.query.ts +1 -1
  120. package/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
  121. package/src/user-data-rights/handlers/request-export.write.ts +2 -2
  122. package/src/user-data-rights/run-export-jobs.ts +2 -2
  123. package/src/user-data-rights/run-forget-cleanup.ts +27 -28
  124. package/src/user-data-rights/run-user-export.ts +1 -1
  125. package/src/user-data-rights/token-helpers.ts +2 -2
  126. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +1 -1
  127. package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +1 -1
@@ -57,7 +57,7 @@ export async function seedUser(db: DbConnection, options: SeedUserOptions): Prom
57
57
  const tdb = createTenantDb(db, by.tenantId, "system");
58
58
 
59
59
  const existing = await fetchOne(db, userTable, eq(userTable["email"], options.email));
60
- if (existing) return existing["id"] as string;
60
+ if (existing) return existing["id"] as string; // @cast-boundary db-row
61
61
 
62
62
  const result = await userExecutor.create(
63
63
  {
@@ -86,7 +86,7 @@ export async function seedUser(db: DbConnection, options: SeedUserOptions): Prom
86
86
  // als ehrlicher Throw rauskommt statt downstream als undefined-Bug.
87
87
  function extractId(data: unknown, who: string): string {
88
88
  if (typeof data === "object" && data !== null && "id" in data) {
89
- const id = (data as { id: unknown }).id;
89
+ const id = (data as { id: unknown }).id; // @cast-boundary engine-bridge
90
90
  if (typeof id === "string") return id;
91
91
  }
92
92
  throw new Error(`${who}: executor.create result has no string id (got ${JSON.stringify(data)})`);
@@ -238,7 +238,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
238
238
  _userId: ctx._userId ?? SYSTEM_USER_ID,
239
239
  };
240
240
  await runExportJobs({
241
- db: ctx.db as import("@cosmicdrift/kumiko-framework/db").DbConnection,
241
+ db: ctx.db as import("@cosmicdrift/kumiko-framework/db").DbConnection, // @cast-boundary db-operator
242
242
  registry: ctx.registry,
243
243
  buildStorageProvider: async (tenantId) =>
244
244
  createFileProviderForTenant(providerCtx, tenantId, "user-data-rights:run-export-jobs"),
@@ -268,11 +268,12 @@ async function mapQueryResponseToRedirect(
268
268
  ): Promise<Response> {
269
269
  if (!queryRes.ok) {
270
270
  const errorBody = await queryRes.text();
271
- return c.body(errorBody, queryRes.status as 400 | 401 | 404 | 410 | 500, {
271
+ const statusCode = queryRes.status as 400 | 401 | 404 | 410 | 500; // @cast-boundary engine-payload
272
+ return c.body(errorBody, statusCode, {
272
273
  "content-type": queryRes.headers.get("content-type") ?? "application/json",
273
274
  });
274
275
  }
275
- const body = (await queryRes.json()) as { data?: { url?: string } };
276
+ const body = (await queryRes.json()) as { data?: { url?: string } }; // @cast-boundary engine-payload
276
277
  if (!body.data?.url) {
277
278
  return c.json({ error: "download_resolution_failed" }, 500);
278
279
  }
@@ -77,7 +77,7 @@ export const cancelDeletionWrite = defineWriteHandler({
77
77
  data: {
78
78
  userId: event.user.id,
79
79
  status: USER_STATUS.Active,
80
- gracePeriodEnd: null as string | null,
80
+ gracePeriodEnd: null as string | null, // @cast-boundary generic-record
81
81
  },
82
82
  };
83
83
  },
@@ -80,11 +80,11 @@ export const downloadByJobQuery = defineQueryHandler({
80
80
  ctx.db.raw,
81
81
  exportJobsTable,
82
82
  eq(exportJobsTable["id"], jobId),
83
- )) as JobRow | null;
83
+ )) as JobRow | null; // @cast-boundary db-row
84
84
 
85
85
  if (!jobRow || jobRow.userId !== userId) {
86
86
  await recordInvalidAttempt({
87
- db: ctx.db.raw as DbConnection,
87
+ db: ctx.db.raw as DbConnection, // @cast-boundary db-runner
88
88
  tenantId,
89
89
  now,
90
90
  result: "notFound",
@@ -102,7 +102,7 @@ export const downloadByJobQuery = defineQueryHandler({
102
102
 
103
103
  if (jobRow.status !== EXPORT_JOB_STATUS.Done) {
104
104
  await recordInvalidAttempt({
105
- db: ctx.db.raw as DbConnection,
105
+ db: ctx.db.raw as DbConnection, // @cast-boundary db-runner
106
106
  tenantId,
107
107
  now,
108
108
  result: "failed",
@@ -119,7 +119,7 @@ export const downloadByJobQuery = defineQueryHandler({
119
119
  }
120
120
  if (!jobRow.downloadStorageKey) {
121
121
  await recordInvalidAttempt({
122
- db: ctx.db.raw as DbConnection,
122
+ db: ctx.db.raw as DbConnection, // @cast-boundary db-runner
123
123
  tenantId,
124
124
  now,
125
125
  result: "expired",
@@ -142,7 +142,7 @@ export const downloadByJobQuery = defineQueryHandler({
142
142
  );
143
143
  if (!provider.getSignedUrl) {
144
144
  await recordInvalidAttempt({
145
- db: ctx.db.raw as DbConnection,
145
+ db: ctx.db.raw as DbConnection, // @cast-boundary db-runner
146
146
  tenantId,
147
147
  now,
148
148
  result: "signedUrlNotSupported",
@@ -177,20 +177,17 @@ export const downloadByJobQuery = defineQueryHandler({
177
177
  ctx.db.raw,
178
178
  exportDownloadTokensTable,
179
179
  eq(exportDownloadTokensTable["jobId"], jobId),
180
- )) as TokenRow | null;
180
+ )) as TokenRow | null; // @cast-boundary db-row
181
181
 
182
182
  if (tokenRow) {
183
183
  await recordDownloadUse({
184
- // @cast-boundary db: ctx.db.raw ist DbRunner, im query-pfad immer
185
- // Connection. Cast legit weil recordDownloadUse intern createTenantDb
186
- // braucht.
187
- db: ctx.db.raw as DbConnection,
184
+ db: ctx.db.raw as DbConnection, // @cast-boundary db-runner
188
185
  tokenId: tokenRow.id,
189
186
  tokenVersion: tokenRow.version,
190
187
  tokenUseCount: tokenRow.useCount ?? 0,
191
188
  tenantId: jobRow.requestedFromTenantId as Parameters<
192
189
  typeof recordDownloadUse
193
- >[0]["tenantId"],
190
+ >[0]["tenantId"], // @cast-boundary engine-bridge
194
191
  now,
195
192
  ip: query.payload.auditMeta?.ip ?? null,
196
193
  userAgent: query.payload.auditMeta?.userAgent ?? null,
@@ -91,7 +91,7 @@ export const downloadByTokenQuery = defineQueryHandler({
91
91
  ctx.db.raw,
92
92
  exportDownloadTokensTable,
93
93
  eq(exportDownloadTokensTable["tokenHash"], hash),
94
- )) as TokenRow | null;
94
+ )) as TokenRow | null; // @cast-boundary db-row
95
95
 
96
96
  if (!tokenRow) {
97
97
  // Invalid token — 404 ohne Existenz-Leak. Generic NotFoundError
@@ -120,13 +120,14 @@ export const downloadByTokenQuery = defineQueryHandler({
120
120
  ctx.db.raw,
121
121
  exportJobsTable,
122
122
  eq(exportJobsTable["id"], tokenRow.jobId),
123
- )) as { requestedFromTenantId: string } | null;
123
+ )) as { requestedFromTenantId: string } | null; // @cast-boundary db-row
124
124
  if (jobForAudit) {
125
+ const auditDb = ctx.db.raw as DbConnection; // @cast-boundary db-runner
125
126
  await recordInvalidAttempt({
126
- db: ctx.db.raw as DbConnection,
127
+ db: auditDb,
127
128
  tenantId: jobForAudit.requestedFromTenantId as Parameters<
128
129
  typeof recordInvalidAttempt
129
- >[0]["tenantId"],
130
+ >[0]["tenantId"], // @cast-boundary engine-bridge
130
131
  now,
131
132
  result: "expired",
132
133
  via: "token",
@@ -147,7 +148,7 @@ export const downloadByTokenQuery = defineQueryHandler({
147
148
  ctx.db.raw,
148
149
  exportJobsTable,
149
150
  eq(exportJobsTable["id"], tokenRow.jobId),
150
- )) as JobRow | null;
151
+ )) as JobRow | null; // @cast-boundary db-row
151
152
 
152
153
  if (!jobRow) {
153
154
  throw new NotFoundError("export-download", undefined, {
@@ -156,10 +157,10 @@ export const downloadByTokenQuery = defineQueryHandler({
156
157
  }
157
158
  if (jobRow.status !== EXPORT_JOB_STATUS.Done) {
158
159
  await recordInvalidAttempt({
159
- db: ctx.db.raw as DbConnection,
160
+ db: ctx.db.raw as DbConnection, // @cast-boundary db-runner
160
161
  tenantId: jobRow.requestedFromTenantId as Parameters<
161
162
  typeof recordInvalidAttempt
162
- >[0]["tenantId"],
163
+ >[0]["tenantId"], // @cast-boundary engine-bridge
163
164
  now,
164
165
  result: "failed",
165
166
  via: "token",
@@ -175,10 +176,10 @@ export const downloadByTokenQuery = defineQueryHandler({
175
176
  }
176
177
  if (!jobRow.downloadStorageKey) {
177
178
  await recordInvalidAttempt({
178
- db: ctx.db.raw as DbConnection,
179
+ db: ctx.db.raw as DbConnection, // @cast-boundary db-runner
179
180
  tenantId: jobRow.requestedFromTenantId as Parameters<
180
181
  typeof recordInvalidAttempt
181
- >[0]["tenantId"],
182
+ >[0]["tenantId"], // @cast-boundary engine-bridge
182
183
  now,
183
184
  result: "expired",
184
185
  via: "token",
@@ -202,10 +203,10 @@ export const downloadByTokenQuery = defineQueryHandler({
202
203
  );
203
204
  if (!provider.getSignedUrl) {
204
205
  await recordInvalidAttempt({
205
- db: ctx.db.raw as DbConnection,
206
+ db: ctx.db.raw as DbConnection, // @cast-boundary db-runner
206
207
  tenantId: jobRow.requestedFromTenantId as Parameters<
207
208
  typeof recordInvalidAttempt
208
- >[0]["tenantId"],
209
+ >[0]["tenantId"], // @cast-boundary engine-bridge
209
210
  now,
210
211
  result: "signedUrlNotSupported",
211
212
  via: "token",
@@ -235,14 +236,11 @@ export const downloadByTokenQuery = defineQueryHandler({
235
236
  // Wrapper (trusted-source). Direct-API-caller koennen luegen, aber
236
237
  // Audit ist nicht security-relevant.
237
238
  await recordDownloadUse({
238
- // @cast-boundary db: ctx.db.raw ist DbRunner (Connection|Tx),
239
- // im query-handler-Pfad ist es immer Connection. Cast legit weil
240
- // recordDownloadUse intern createTenantDb braucht (DbConnection).
241
- db: ctx.db.raw as DbConnection,
239
+ db: ctx.db.raw as DbConnection, // @cast-boundary db-runner
242
240
  tokenId: tokenRow.id,
243
241
  tokenVersion: tokenRow.version,
244
242
  tokenUseCount: tokenRow.useCount ?? 0,
245
- tenantId: jobRow.requestedFromTenantId as Parameters<typeof recordDownloadUse>[0]["tenantId"],
243
+ tenantId: jobRow.requestedFromTenantId as Parameters<typeof recordDownloadUse>[0]["tenantId"], // @cast-boundary engine-bridge
246
244
  now,
247
245
  ip: query.payload.auditMeta?.ip ?? null,
248
246
  userAgent: query.payload.auditMeta?.userAgent ?? null,
@@ -55,7 +55,7 @@ export const exportStatusQuery = defineQueryHandler({
55
55
  .from(exportJobsTable)
56
56
  .where(eq(exportJobsTable["userId"], query.user.id))
57
57
  .orderBy(desc(exportJobsTable["requestedAt"]))
58
- .limit(1)) as ExportJobRow[];
58
+ .limit(1)) as ExportJobRow[]; // @cast-boundary db-row
59
59
 
60
60
  const latest = rows[0];
61
61
  if (!latest) return { hasJob: false as const };
@@ -69,7 +69,7 @@ export function createRequestDeletionHandler(opts: RequestDeletionOptions = {})
69
69
  createSystemUser(event.user.tenantId),
70
70
  "compliance-profiles:query:for-tenant",
71
71
  {},
72
- )) as { profile: { userRights: { gracePeriod: DurationSpec } } };
72
+ )) as { profile: { userRights: { gracePeriod: DurationSpec } } }; // @cast-boundary engine-payload
73
73
 
74
74
  // addDurationSpec deckt `{days}` und `{hours}` ab. App-Server-Clock
75
75
  // ist authoritative — instant() customType nimmt Temporal.Instant
@@ -50,7 +50,7 @@ function isActiveJobConflict(failure: WriteFailure): boolean {
50
50
  const error = failure.error as {
51
51
  code?: string;
52
52
  details?: { constraintName?: string };
53
- };
53
+ }; // @cast-boundary error-details
54
54
  return (
55
55
  error.code === "unique_violation" && error.details?.constraintName === ACTIVE_JOB_CONSTRAINT
56
56
  );
@@ -124,7 +124,7 @@ export const requestExportWrite = defineWriteHandler({
124
124
  // Happy path: neuer Job. SaveContext.id ist EntityId (number | string);
125
125
  // exportJobEntity hat idType:"uuid" → garantiert string, String()
126
126
  // schuetzt vor Drift falls jemand die Entity auf serial migriert.
127
- const created = result.data as SaveContext;
127
+ const created = result.data as SaveContext; // @cast-boundary engine-payload
128
128
  return {
129
129
  isSuccess: true as const,
130
130
  data: {
@@ -449,7 +449,7 @@ async function processJob(args: {
449
449
  // persistiert).
450
450
  throw new Error(
451
451
  `Job ${job.id}: Token-Creation failed before done-flip. ` +
452
- `${(tokenCreateResult as { error?: { code?: string } }).error?.code ?? "unknown"}`,
452
+ `${(tokenCreateResult as { error?: { code?: string } }).error?.code ?? "unknown"}`, // @cast-boundary engine-payload
453
453
  );
454
454
  }
455
455
 
@@ -479,7 +479,7 @@ async function processJob(args: {
479
479
  // gecleared.
480
480
  throw new Error(
481
481
  `Job ${job.id}: failed to flip status=done after successful Token-Create. ` +
482
- `${(doneResult as { error?: { code?: string } }).error?.code ?? "unknown"}`,
482
+ `${(doneResult as { error?: { code?: string } }).error?.code ?? "unknown"}`, // @cast-boundary engine-payload
483
483
  );
484
484
  }
485
485
 
@@ -131,7 +131,7 @@ export async function runForgetCleanup(
131
131
  const usages = registry.getExtensionUsages(EXT_USER_DATA);
132
132
  const hookEntries: HookEntry[] = usages
133
133
  .map((u): HookEntry | null => {
134
- const opts = (u.options ?? {}) as { delete?: UserDataDeleteHook };
134
+ const opts = (u.options ?? {}) as { delete?: UserDataDeleteHook }; // @cast-boundary engine-payload
135
135
  return opts.delete ? { entityName: u.entityName, deleteHook: opts.delete } : null;
136
136
  })
137
137
  .filter((x): x is HookEntry => x !== null);
@@ -259,36 +259,35 @@ async function processUser(args: {
259
259
  let currentTenantId: TenantId | null = null;
260
260
  let currentEntityName: string | null = null;
261
261
  try {
262
- await (
263
- db as { transaction: (fn: (tx: DbRunner) => Promise<void>) => Promise<void> }
264
- ).transaction(async (tx) => {
265
- for (const tenantId of tenantList) {
266
- currentTenantId = tenantId;
267
- for (const entry of hookEntries) {
268
- currentEntityName = entry.entityName;
269
- const policy = await resolveRetentionPolicyForTenant({
270
- db: tx,
271
- registry,
272
- tenantId,
273
- entityName: entry.entityName,
274
- });
275
- const strategy = policyToStrategy(policy.policy?.strategy ?? null);
262
+ await (db as { transaction: (fn: (tx: DbRunner) => Promise<void>) => Promise<void> }) // @cast-boundary db-runner
263
+ .transaction(async (tx) => {
264
+ for (const tenantId of tenantList) {
265
+ currentTenantId = tenantId;
266
+ for (const entry of hookEntries) {
267
+ currentEntityName = entry.entityName;
268
+ const policy = await resolveRetentionPolicyForTenant({
269
+ db: tx,
270
+ registry,
271
+ tenantId,
272
+ entityName: entry.entityName,
273
+ });
274
+ const strategy = policyToStrategy(policy.policy?.strategy ?? null);
276
275
 
277
- hookCallsAttempted++;
278
- await entry.deleteHook({ db: tx, tenantId, userId }, strategy);
276
+ hookCallsAttempted++;
277
+ await entry.deleteHook({ db: tx, tenantId, userId }, strategy);
278
+ }
279
279
  }
280
- }
281
280
 
282
- // Status-Flip in derselben Sub-Tx. Falls einer der Hooks oben
283
- // geworfen hat, kommen wir hier nicht an — die Tx rollback'd
284
- // alles, der User bleibt im DeletionRequested-Status, naechster
285
- // Run retried.
286
- await tx
287
- .update(userTable)
288
- .set({ status: USER_STATUS.Deleted })
289
- .where(eq(userTable["id"], userId));
290
- txSucceeded = true;
291
- });
281
+ // Status-Flip in derselben Sub-Tx. Falls einer der Hooks oben
282
+ // geworfen hat, kommen wir hier nicht an — die Tx rollback'd
283
+ // alles, der User bleibt im DeletionRequested-Status, naechster
284
+ // Run retried.
285
+ await tx
286
+ .update(userTable)
287
+ .set({ status: USER_STATUS.Deleted })
288
+ .where(eq(userTable["id"], userId));
289
+ txSucceeded = true;
290
+ });
292
291
  } catch (e) {
293
292
  // currentTenantId/currentEntityName tracken den Failing-Hook —
294
293
  // Operator sieht "Hook fileRef in Tenant A failed for user X" statt
@@ -128,7 +128,7 @@ export async function runUserExport(args: RunUserExportArgs): Promise<UserExport
128
128
  const usages = registry.getExtensionUsages(EXT_USER_DATA);
129
129
  const hookEntries: HookEntry[] = usages
130
130
  .map((u): HookEntry | null => {
131
- const opts = (u.options ?? {}) as { export?: UserDataExportHook };
131
+ const opts = (u.options ?? {}) as { export?: UserDataExportHook }; // @cast-boundary engine-payload
132
132
  return opts.export ? { entityName: u.entityName, exportHook: opts.export } : null;
133
133
  })
134
134
  .filter((x): x is HookEntry => x !== null);
@@ -53,7 +53,7 @@ function uint8ArrayToBase64Url(bytes: Uint8Array): string {
53
53
  // btoa erwartet binary-string. atob/btoa sind universal in Bun + Node + Browser.
54
54
  let binary = "";
55
55
  for (let i = 0; i < bytes.length; i++) {
56
- binary += String.fromCharCode(bytes[i] as number);
56
+ binary += String.fromCharCode(bytes[i] as number); // @cast-boundary dynamic-key
57
57
  }
58
58
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
59
59
  }
@@ -61,7 +61,7 @@ function uint8ArrayToBase64Url(bytes: Uint8Array): string {
61
61
  function uint8ArrayToHex(bytes: Uint8Array): string {
62
62
  let out = "";
63
63
  for (let i = 0; i < bytes.length; i++) {
64
- out += (bytes[i] as number).toString(16).padStart(2, "0");
64
+ out += (bytes[i] as number).toString(16).padStart(2, "0"); // @cast-boundary dynamic-key
65
65
  }
66
66
  return out;
67
67
  }
@@ -45,7 +45,7 @@ export const fileRefExportHook: UserDataExportHook = async (ctx) => {
45
45
  // .toString() funktioniert sowohl auf Temporal.Instant als auch
46
46
  // Date.
47
47
  const rows = rawRows.map((r) => {
48
- const row = r as Record<string, unknown>;
48
+ const row = r as Record<string, unknown>; // @cast-boundary recursive-walk
49
49
  return {
50
50
  id: String(row["id"]),
51
51
  storageKey: String(row["storageKey"]),
@@ -38,7 +38,7 @@ export const userExportHook: UserDataExportHook = async (ctx) => {
38
38
  displayName: string;
39
39
  locale: string;
40
40
  emailVerified: boolean;
41
- } | null;
41
+ } | null; // @cast-boundary db-runner
42
42
 
43
43
  if (!row) return null;
44
44