@cosmicdrift/kumiko-bundled-features 0.2.3 → 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 +60 -0
- package/package.json +17 -14
- 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 +1 -1
- 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/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/handlers/for-tenant.query.ts +7 -6
- package/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
- package/src/compliance-profiles/handlers/set-profile.write.ts +6 -8
- package/src/compliance-profiles/resolve-for-tenant.ts +7 -5
- package/src/compliance-profiles/seeding.ts +1 -1
- package/src/config/resolver.ts +1 -1
- package/src/data-retention/_internal/parse-override.ts +3 -2
- package/src/data-retention/handlers/policy-for.query.ts +1 -1
- package/src/data-retention/keep-for.ts +1 -1
- package/src/data-retention/presets.ts +1 -1
- package/src/data-retention/resolve-for-tenant.ts +1 -1
- 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 +1 -1
- package/src/file-provider-s3/feature.ts +2 -2
- package/src/files-provider-s3/s3-provider.ts +2 -2
- 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/handlers/rotate.job.ts +2 -2
- package/src/sessions/handlers/cleanup.job.ts +2 -2
- 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/feature.ts +8 -8
- package/src/user/handlers/find-for-auth.query.ts +1 -1
- package/src/user/seeding.ts +2 -2
- package/src/user-data-rights/feature.ts +4 -3
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
- package/src/user-data-rights/handlers/download-by-job.query.ts +8 -11
- package/src/user-data-rights/handlers/download-by-token.query.ts +14 -16
- package/src/user-data-rights/handlers/export-status.query.ts +1 -1
- package/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
- package/src/user-data-rights/handlers/request-export.write.ts +2 -2
- package/src/user-data-rights/run-export-jobs.ts +2 -2
- package/src/user-data-rights/run-forget-cleanup.ts +27 -28
- package/src/user-data-rights/run-user-export.ts +1 -1
- package/src/user-data-rights/token-helpers.ts +2 -2
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +1 -1
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +1 -1
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
278
|
-
|
|
276
|
+
hookCallsAttempted++;
|
|
277
|
+
await entry.deleteHook({ db: tx, tenantId, userId }, strategy);
|
|
278
|
+
}
|
|
279
279
|
}
|
|
280
|
-
}
|
|
281
280
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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"]),
|