@cosmicdrift/kumiko-samples 0.404.0 → 0.405.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/package.json +1 -1
- package/packages/bundled-features/package.json +25 -25
- package/packages/bundled-features/src/account-security/README.md +7 -5
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +11 -47
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -0
- package/packages/bundled-features/src/auth-email-password/web/default-topbar-actions.tsx +9 -1
- package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
- package/packages/bundled-features/src/auth-email-password/web/invite-accept-screen.tsx +10 -41
- package/packages/bundled-features/src/auth-email-password/web/tenant-label.ts +15 -0
- package/packages/bundled-features/src/auth-email-password/web/tenant-menu-items.tsx +2 -7
- package/packages/bundled-features/src/auth-email-password/web/tenant-switcher.tsx +7 -12
- package/packages/bundled-features/src/auth-email-password/web/use-redirect-authed-from-login.ts +39 -0
- package/packages/bundled-features/src/auth-foundation/feature.ts +8 -21
- package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
- package/packages/bundled-features/src/auth-mfa/handlers/reencrypt.job.ts +9 -15
- package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +17 -14
- package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +6 -8
- package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +6 -8
- package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +16 -57
- package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +3 -13
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -5
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -8
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -8
- package/packages/bundled-features/src/billing-foundation/index.ts +8 -0
- package/packages/bundled-features/src/billing-foundation/projection.ts +19 -33
- package/packages/bundled-features/src/billing-foundation/provider-event-seen.ts +14 -0
- package/packages/bundled-features/src/billing-foundation/provider-shared.ts +61 -0
- package/packages/bundled-features/src/billing-foundation/tenant-destroy-hook.ts +8 -4
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/config/handlers/reencrypt.job.ts +9 -15
- package/packages/bundled-features/src/delivery/delivery-service.ts +2 -10
- package/packages/bundled-features/src/documents/changes.json +8 -0
- package/packages/bundled-features/src/file-derivatives/feature.ts +9 -21
- package/packages/bundled-features/src/file-foundation/feature.ts +2 -6
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +1 -6
- package/packages/bundled-features/src/folders/feature.ts +2 -8
- package/packages/bundled-features/src/folders/web/folder-manager.tsx +7 -32
- package/packages/bundled-features/src/folders/web/folder-section.tsx +5 -25
- package/packages/bundled-features/src/form-draft/handlers/save.write.ts +6 -16
- package/packages/bundled-features/src/host-routing/README.md +10 -10
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +2 -6
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +1 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +5 -13
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +9 -19
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +1 -15
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +1 -14
- package/packages/bundled-features/src/jobs/job-run-logger.ts +37 -40
- package/packages/bundled-features/src/jobs/parse-subject.ts +15 -0
- package/packages/bundled-features/src/ledger/feature.ts +2 -7
- package/packages/bundled-features/src/legal-pages/README.md +21 -13
- package/packages/bundled-features/src/mail-foundation/feature.ts +3 -7
- package/packages/bundled-features/src/managed-pages/render-page-body.ts +4 -12
- package/packages/bundled-features/src/notes-history/feature.ts +7 -8
- package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +1 -9
- package/packages/bundled-features/src/personal-access-tokens/hash.ts +2 -2
- package/packages/bundled-features/src/personal-access-tokens/parse-scope-names.ts +9 -0
- package/packages/bundled-features/src/personal-access-tokens/resolver.ts +1 -9
- package/packages/bundled-features/src/renderer-foundation/README.md +6 -4
- package/packages/bundled-features/src/renderer-foundation/feature.ts +3 -16
- package/packages/bundled-features/src/renderer-pdf/README.md +5 -1
- package/packages/bundled-features/src/secrets/handlers/rotate.job.ts +13 -19
- package/packages/bundled-features/src/shared/changes.json +6 -0
- package/packages/bundled-features/src/shared/collect-extension-options.ts +14 -0
- package/packages/bundled-features/src/shared/hmac-signature.ts +15 -0
- package/packages/bundled-features/src/shared/index.ts +6 -0
- package/packages/bundled-features/src/shared/is-unique-constraint-failure.ts +11 -0
- package/packages/bundled-features/src/shared/mail-locale-templates.ts +64 -0
- package/packages/bundled-features/src/shared/resolve-feature-access.ts +16 -0
- package/packages/bundled-features/src/shared/signed-token.ts +4 -14
- package/packages/bundled-features/src/shared/single-use-token-store.ts +2 -2
- package/packages/bundled-features/src/shared/stream-tenant-derived-state.ts +5 -8
- package/packages/bundled-features/src/shared/tenant-db-cache.ts +19 -0
- package/packages/bundled-features/src/shared/web/status-banner.tsx +20 -0
- package/packages/bundled-features/src/shared/web/use-dispatched-writes.ts +31 -0
- package/packages/bundled-features/src/subscription-paddle/feature.ts +5 -17
- package/packages/bundled-features/src/subscription-paddle/price-methods.ts +7 -29
- package/packages/bundled-features/src/subscription-paddle/runtime.ts +6 -17
- package/packages/bundled-features/src/subscription-stripe/feature.ts +5 -19
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +5 -20
- package/packages/bundled-features/src/subscription-stripe/runtime.ts +3 -19
- package/packages/bundled-features/src/tags/feature.ts +2 -8
- package/packages/bundled-features/src/tags/web/tag-manager.tsx +5 -25
- package/packages/bundled-features/src/tags/web/tag-section.tsx +3 -18
- package/packages/bundled-features/src/template-resolver/README.md +29 -29
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +3 -10
- package/packages/bundled-features/src/template-resolver/web/collection-entry-create-form.tsx +1 -0
- package/packages/bundled-features/src/tenant/handlers/decrypt-member-users.ts +30 -0
- package/packages/bundled-features/src/tenant/handlers/members.query.ts +3 -23
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +8 -20
- package/packages/bundled-features/src/tenant-lifecycle/archive-and-delete-tenant-rows.ts +28 -0
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/index.ts +5 -0
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +1 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/COMPLIANCE.md +36 -36
- package/packages/bundled-features/src/user-data-rights/README.md +33 -33
- package/packages/bundled-features/src/user-data-rights/build-hook-db.ts +40 -0
- package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +2 -19
- package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +11 -47
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -35
- package/packages/bundled-features/src/user-data-rights/run-user-export.ts +2 -31
- package/packages/bundled-features/src/user-data-rights/token-helpers.ts +3 -15
- package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +1 -18
- package/packages/bundled-features/src/user-data-rights/web/public-deletion-gate.tsx +1 -0
- package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +1 -18
- package/samples/apps/admin-console/README.md +5 -5
- package/samples/apps/admin-console/package.json +3 -3
- package/samples/apps/cap-billing-demo/README.md +15 -15
- package/samples/apps/config-demo/README.md +7 -7
- package/samples/apps/config-demo/package.json +2 -2
- package/samples/apps/marketing-demo/README.md +2 -2
- package/samples/apps/marketing-demo/package.json +2 -2
- package/samples/apps/showcase/package.json +3 -3
- package/samples/apps/styleguide/package.json +2 -2
- package/samples/apps/ui-walkthrough/README.md +8 -9
- package/samples/apps/ui-walkthrough/package.json +2 -2
- package/samples/apps/use-all-bundled/README.md +2 -2
- package/samples/apps/use-all-bundled/package.json +2 -2
- package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +18 -1
- package/samples/apps/user-data-rights-demo/README.md +12 -12
- package/samples/apps/workspaces/package.json +2 -2
- package/samples/recipes/apex-landing/README.md +3 -3
- package/samples/recipes/apex-landing/package.json +1 -1
- package/samples/recipes/apex-surface-auth/README.md +13 -9
- package/samples/recipes/auth-mfa/README.md +2 -4
- package/samples/recipes/auth-mfa/package.json +2 -2
- package/samples/recipes/boot-check/README.md +2 -2
- package/samples/recipes/compliance-profiles-demo/README.md +5 -5
- package/samples/recipes/compose-stacks/README.md +8 -8
- package/samples/recipes/cross-feature-events/README.md +1 -0
- package/samples/recipes/crypto-shredding-hr/README.md +3 -3
- package/samples/recipes/custom-fields-basic/README.md +1 -1
- package/samples/recipes/documents/README.md +25 -0
- package/samples/recipes/embedded-entity-form/package.json +2 -2
- package/samples/recipes/encrypted-tenant-config/README.md +8 -8
- package/samples/recipes/env-composition/README.md +10 -10
- package/samples/recipes/error-contract/README.md +9 -9
- package/samples/recipes/event-sourcing/README.md +9 -9
- package/samples/recipes/ledger-invoicing/README.md +6 -6
- package/samples/recipes/legal-pages/README.md +16 -16
- package/samples/recipes/managed-config/README.md +15 -11
- package/samples/recipes/managed-pages/README.md +7 -7
- package/samples/recipes/pdf-audit-export/package.json +1 -1
- package/samples/recipes/pipeline-basics/README.md +13 -13
- package/samples/recipes/public-share-token/README.md +4 -4
- package/samples/recipes/record-detail-layout/package.json +3 -3
- package/samples/recipes/security-baseline/README.md +4 -1
- package/samples/recipes/session-revocation/README.md +1 -1
- package/samples/recipes/tenant-settings/README.md +4 -4
- package/samples/recipes/tier-admin/README.md +2 -2
- package/samples/recipes/user-data-rights/README.md +21 -19
- package/samples/recipes/wizard-form/package.json +3 -3
- package/samples/recipes/writeform-section/package.json +3 -3
|
@@ -14,8 +14,11 @@ import { decryptStoredPii, mapWithConcurrency } from "../../shared/index.ts";
|
|
|
14
14
|
import { userTable } from "../../user/index.ts";
|
|
15
15
|
import { INVITATION_STATUS, tenantInvitationsTable } from "../invitation-table.ts";
|
|
16
16
|
import { tenantMembershipsTable } from "../membership-table.ts";
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
import {
|
|
18
|
+
decryptMemberUsers,
|
|
19
|
+
KMS_POOL_CONCURRENCY,
|
|
20
|
+
type MemberUserRow,
|
|
21
|
+
} from "./decrypt-member-users.ts";
|
|
19
22
|
|
|
20
23
|
type TeamStatus = "active" | "pending";
|
|
21
24
|
|
|
@@ -115,11 +118,7 @@ export const teamListQuery = definePagedQueryHandler({
|
|
|
115
118
|
const membershipRows = await selectMany(db, tenantMembershipsTable, { tenantId });
|
|
116
119
|
const userIds = [...new Set(membershipRows.map((row) => String(row["userId"])))];
|
|
117
120
|
const users =
|
|
118
|
-
userIds.length > 0
|
|
119
|
-
? await selectMany<{ id: unknown; email?: unknown; displayName?: unknown }>(db, userTable, {
|
|
120
|
-
id: userIds,
|
|
121
|
-
})
|
|
122
|
-
: [];
|
|
121
|
+
userIds.length > 0 ? await selectMany<MemberUserRow>(db, userTable, { id: userIds }) : [];
|
|
123
122
|
|
|
124
123
|
const lastSeenByUserId = new Map<string, Temporal.Instant>();
|
|
125
124
|
if (userIds.length > 0) {
|
|
@@ -147,21 +146,10 @@ export const teamListQuery = definePagedQueryHandler({
|
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
// Bounded, not Promise.all/sequential-per-item: shares the KMS
|
|
150
|
-
// adapter's small dedicated pool (max 4, see
|
|
149
|
+
// adapter's small dedicated pool (max 4, see decrypt-member-users.ts) with the
|
|
151
150
|
// invitations decrypt below — the two passes run one after the other
|
|
152
151
|
// (await, not Promise.all), so combined concurrency never exceeds 4.
|
|
153
|
-
const
|
|
154
|
-
const email =
|
|
155
|
-
typeof user.email === "string"
|
|
156
|
-
? await decryptStoredPii(user.email, "email", "tenant:team-list")
|
|
157
|
-
: null;
|
|
158
|
-
const displayName =
|
|
159
|
-
typeof user.displayName === "string"
|
|
160
|
-
? await decryptStoredPii(user.displayName, "displayName", "tenant:team-list")
|
|
161
|
-
: null;
|
|
162
|
-
return { userId: String(user.id), email, displayName };
|
|
163
|
-
});
|
|
164
|
-
const decryptedByUserId = new Map(decryptedUsers.map((d) => [d.userId, d]));
|
|
152
|
+
const decryptedByUserId = await decryptMemberUsers(users, "tenant:team-list");
|
|
165
153
|
|
|
166
154
|
const memberRows: TeamRow[] = membershipRows.map((row) => {
|
|
167
155
|
const userId = String(row["userId"]);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { declareEscapeHatch, type TenantDataHookCtx } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { archiveStream } from "@cosmicdrift/kumiko-framework/event-store";
|
|
4
|
+
|
|
5
|
+
export const TENANT_DESTROY_ARCHIVED_BY = "tenant-lifecycle:destroy";
|
|
6
|
+
export const TENANT_DESTROY_ARCHIVE_REASON = "tenant_destroy";
|
|
7
|
+
|
|
8
|
+
// Archiving first means a later projection rebuild cannot resurrect the deleted rows.
|
|
9
|
+
export async function archiveAndDeleteTenantRows(
|
|
10
|
+
ctx: TenantDataHookCtx,
|
|
11
|
+
table: EntityTableMeta,
|
|
12
|
+
aggregateType: string,
|
|
13
|
+
escapeHatchReason: string,
|
|
14
|
+
): Promise<readonly string[]> {
|
|
15
|
+
declareEscapeHatch({ reason: escapeHatchReason });
|
|
16
|
+
const rows = await ctx.db.selectMany<{ id: string }>(table, { tenantId: ctx.tenantId });
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
await archiveStream(ctx.db.unsafeRaw(), {
|
|
19
|
+
tenantId: ctx.tenantId,
|
|
20
|
+
aggregateId: row.id,
|
|
21
|
+
aggregateType,
|
|
22
|
+
archivedBy: TENANT_DESTROY_ARCHIVED_BY,
|
|
23
|
+
reason: TENANT_DESTROY_ARCHIVE_REASON,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
await ctx.db.deleteMany(table, { tenantId: ctx.tenantId });
|
|
27
|
+
return rows.map((row) => row.id);
|
|
28
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.405.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "archiveAndDeleteTenantRows for tenant-destroy hooks",
|
|
6
|
+
"detail": "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle exports archiveAndDeleteTenantRows(ctx, table, aggregateType, escapeHatchReason), which archives each row's event stream and then deletes the tenant's rows, plus the TENANT_DESTROY_ARCHIVED_BY and TENANT_DESTROY_ARCHIVE_REASON constants. inbound-mail-foundation uses the helper, billing-foundation the constants."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.389.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -92,6 +92,7 @@ const tenantMembershipCrud = createEventStoreExecutor(
|
|
|
92
92
|
// earlier hook that keeps reporting done:false can use up the budget, so later chunking
|
|
93
93
|
// hooks time out with nothing processed until it finishes.
|
|
94
94
|
function newOutcomeAccumulator(): {
|
|
95
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: takes the awaited result of a destroy hook, whose public type allows void
|
|
95
96
|
add: (result: TenantDestroyHookResult | void) => void;
|
|
96
97
|
outcome: () => StageRunOutcome;
|
|
97
98
|
} {
|
|
@@ -495,7 +495,7 @@ export function createTierEngineFeature<
|
|
|
495
495
|
// point AND registers itself as the default plugin. Other features could
|
|
496
496
|
// register there later (e.g. a future toggle-only resolver), but only one
|
|
497
497
|
// plugin wins — see the "single-plugin" comment on findTierResolverUsage.
|
|
498
|
-
r.extendsRegistrar(TENANT_TIER_RESOLVER_EXT, {
|
|
498
|
+
r.extendsRegistrar(TENANT_TIER_RESOLVER_EXT, {});
|
|
499
499
|
|
|
500
500
|
// Plugin-registration: framework's runDevApp/runProdApp picks this up
|
|
501
501
|
// post-stack-setup, calls build(deps), wires effectiveFeatures.
|
|
@@ -16,34 +16,34 @@ Dieses Dokument bündelt die **technischen Fakten** zur DSGVO-Pipeline
|
|
|
16
16
|
|
|
17
17
|
## 1. Endpoint → Artikel-Mapping
|
|
18
18
|
|
|
19
|
-
| Artikel
|
|
20
|
-
|
|
21
|
-
| **Art. 15 (Auskunft, light)**
|
|
22
|
-
| **Art. 15 + 20 (Auskunft + Portabilität)** | `user-data-rights:write:request-export`
|
|
23
|
-
| **Art. 15 + 20**
|
|
24
|
-
| **Art. 15 + 20**
|
|
25
|
-
| **Art. 17 (Löschung)**
|
|
26
|
-
| **Art. 17**
|
|
27
|
-
| **Art. 17**
|
|
28
|
-
| **Art. 18 (Restriction)**
|
|
29
|
-
| **Art. 18**
|
|
30
|
-
| **Operator (DPO)**
|
|
19
|
+
| Artikel | Endpoint / Runner | Wer ruft auf | Was passiert |
|
|
20
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| **Art. 15 (Auskunft, light)** | `user-data-rights:query:my-audit-log` | User | Liest eigene Framework-Events aus `kumiko_events` (account-weit über alle Memberships). Filterbar nach `eventType`, `aggregateType`, `from`/`to`. Domain-Entities ohne `ctx.events.append` erscheinen NICHT — die kommen im Export-Bundle (Art. 20). |
|
|
22
|
+
| **Art. 15 + 20 (Auskunft + Portabilität)** | `user-data-rights:write:request-export` | User | Async Job → ZIP mit user-Profil + fileRefs + alle EXT_USER_DATA-Provider-Daten (cross-tenant) + signed Magic-Link per Email. Idempotent: nur 1 active Job pro User (`ACTIVE_JOB_CONSTRAINT`). |
|
|
23
|
+
| **Art. 15 + 20** | `GET /user-export/by-token` (interstitial) + `POST /user-export/by-token` (exchange) | Anonym (Magic-Link-Pfad) | Email-Link trägt den Token als URL-Fragment (`#token=`, nie an den Server gesendet, issue #1271); Interstitial-Script liest das Fragment client-seitig und tauscht es per POST-Body gegen die signed Storage-URL. Kein `?token=`-Query-Fallback (#1512) — Query-Param landet unveränderlich in Proxy-/Access-Logs. Multi-use innerhalb TTL. Audit: `lastUsedAt`, `lastUsedFromIp`, `lastUsedUserAgent`. |
|
|
24
|
+
| **Art. 15 + 20** | `GET /user-export/by-job/:jobId` | User (Session-Auth) | UI-Klick-Pfad. Cross-tenant-same-user: User in Tenant B kann Job aus Tenant A laden wenn er der Owner ist. |
|
|
25
|
+
| **Art. 17 (Löschung)** | `user-data-rights:write:request-deletion` | User | Soft-Delete: `status=DeletionRequested` + `gracePeriodEnd = now + profile.gracePeriod`. Cron `run-forget-cleanup` führt nach Grace die Anonymisierung durch. |
|
|
26
|
+
| **Art. 17** | `user-data-rights:write:cancel-deletion` | User | Während Grace: status zurück auf `Active`. |
|
|
27
|
+
| **Art. 17** | `run-forget-cleanup` (Cron) | System | Findet User mit `status=DeletionRequested AND gracePeriodEnd < now`. Pro User Sub-TX über alle EXT_USER_DATA-Provider mit Strategy aus `retention.policyFor`. user-Hook anonymisiert (PII raus, Sentinel-Email). |
|
|
28
|
+
| **Art. 18 (Restriction)** | `user-data-rights:write:restrict-account` | Admin / SystemAdmin | Status-Flip → Auth-Middleware-Guard blockt Logins. `sessions.revokeAllForUser` killt aktive Sessions. |
|
|
29
|
+
| **Art. 18** | `user-data-rights:write:lift-restriction` | Admin / SystemAdmin | Restriction aufheben. |
|
|
30
|
+
| **Operator (DPO)** | `user-data-rights:query:list-download-attempts` | Admin / SystemAdmin | Brute-Force-Detection: zeigt invalid Download-Versuche (notFound / expired / failed / signedUrlNotSupported) gefiltert nach Result, IP, Zeitraum. |
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
34
|
## 2. Speicherorte (Verarbeitungsverzeichnis Art. 30)
|
|
35
35
|
|
|
36
|
-
| Tabelle
|
|
37
|
-
|
|
38
|
-
| `read_users`
|
|
39
|
-
| `read_tenant_memberships`
|
|
40
|
-
| `kumiko_events`
|
|
41
|
-
| `read_export_jobs`
|
|
42
|
-
| `read_export_download_tokens`
|
|
43
|
-
| `read_download_attempts`
|
|
44
|
-
| `read_tenant_compliance_profiles` | Per-Tenant Profile-Wahl + Override
|
|
45
|
-
| `read_tenant_retention_overrides` | Per-Tenant Retention-Override pro Entity
|
|
46
|
-
| Storage-Provider (Local / S3)
|
|
36
|
+
| Tabelle | Inhalt | Personenbezug | Retention | Zweck |
|
|
37
|
+
| --------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
38
|
+
| `read_users` | User-Profil (email, displayName, passwordHash, locale, status, gracePeriodEnd, roles) | direkt | per Domain (typ. blockDelete bei Aufbewahrungspflicht, sonst hardDelete via Forget-Pipeline) | Authentifizierung, Nutzer-Profil |
|
|
39
|
+
| `read_tenant_memberships` | User↔Tenant-Verknüpfung + Rollen | direkt (via userId) | per Tenant-Lifecycle | Mehrmandant-Zuordnung |
|
|
40
|
+
| `kumiko_events` | Event-Store (alle write-Events) | direkt (`createdBy = userId`) | per Domain-Policy via `data-retention` | Audit-Trail (Art. 15-Selbstauskunft Quelle), Event-Sourcing |
|
|
41
|
+
| `read_export_jobs` | Async Export-Status (queued/running/done/failed), userId, requestedAt, doneAt, storageKey | direkt (userId) | per `compliance-profiles` Profil-Default | Idempotenz + Status-Polling |
|
|
42
|
+
| `read_export_download_tokens` | Magic-Link-Hash (SHA-256), TTL, useCount, lastUsedAt, lastUsedFromIp, lastUsedUserAgent | indirekt (Token→Job→User) | `compliance-profiles.userRights.exportDownloadTtl` (default 7d) | Magic-Link-Auth + Multi-Use-Audit |
|
|
43
|
+
| `read_download_attempts` | Invalid Download-Versuche: result, via, tokenHash, ip, userAgent, attemptedAt | indirekt (IP) | **90d hardDelete** (Entity-Default, Disk-Bomb-Schutz) | DPO-Brute-Force-Detection |
|
|
44
|
+
| `read_tenant_compliance_profiles` | Per-Tenant Profile-Wahl + Override | nein | unbounded (Konfiguration) | Region-/Branchen-Defaults |
|
|
45
|
+
| `read_tenant_retention_overrides` | Per-Tenant Retention-Override pro Entity | nein | unbounded (Konfiguration) | Aufbewahrungspflicht-Edge-Cases |
|
|
46
|
+
| Storage-Provider (Local / S3) | Export-ZIPs + File-Binaries | indirekt (Inhalt) | Local: per `exportDownloadTtl` Cleanup. S3: lifecycle-Policy (App-Author-Verantwortung) | Daten-Export-Auslieferung |
|
|
47
47
|
|
|
48
48
|
### Bekanntes Residuum: `createdBy` / `metadata.userId` in `kumiko_events`
|
|
49
49
|
|
|
@@ -69,12 +69,12 @@ Retention-Policy irgendwann das Event selbst löscht.
|
|
|
69
69
|
|
|
70
70
|
## 3. Compliance-Profile
|
|
71
71
|
|
|
72
|
-
| Profil
|
|
73
|
-
|
|
74
|
-
| `eu-dsgvo`
|
|
75
|
-
| `swiss-dsg`
|
|
76
|
-
| `de-hr-dsgvo-hgb`
|
|
77
|
-
| `minimal-no-region` | 30d
|
|
72
|
+
| Profil | gracePeriod | exportDownloadTtl | Stale-After | Sub-Processors |
|
|
73
|
+
| ------------------- | ----------- | ----------------- | ----------- | -------------------------------------------------------------- |
|
|
74
|
+
| `eu-dsgvo` | 30d | 7d | 30d | per Tenant konfigurierbar |
|
|
75
|
+
| `swiss-dsg` | 30d | 7d | 30d | + EDÖB-Meldepfad |
|
|
76
|
+
| `de-hr-dsgvo-hgb` | 30d | 7d | 30d | + HR-Aufbewahrung 10y für HR-Entities (anonymize statt delete) |
|
|
77
|
+
| `minimal-no-region` | 30d | 7d | 30d | Migration-Edge-Case ohne Region |
|
|
78
78
|
|
|
79
79
|
Tenant kann via `compliance-profiles:write:set-profile` + Override (`override.userRights.gracePeriod={ days: N }` etc.) eigene Werte setzen.
|
|
80
80
|
|
|
@@ -128,13 +128,13 @@ und Organisatorische Maßnahmen" übernommen werden:
|
|
|
128
128
|
|
|
129
129
|
## 5. Cron-Jobs / Operationale Trigger
|
|
130
130
|
|
|
131
|
-
| Job
|
|
132
|
-
|
|
133
|
-
| `run-forget-cleanup`
|
|
134
|
-
| `run-export-jobs`
|
|
135
|
-
| `data-retention-cleanup` | Cron (in `data-retention`)
|
|
136
|
-
| Token-Cleanup (implizit) | Per `compliance-profiles.userRights.exportDownloadTtl` | Worker-Job entfernt expired Magic-Link-Tokens + Storage-Binaries
|
|
137
|
-
| Download-Attempt-Cleanup | Per Entity-Default 90d
|
|
131
|
+
| Job | Trigger | Was passiert | Operator-Sichtbarkeit |
|
|
132
|
+
| ------------------------ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
133
|
+
| `run-forget-cleanup` | Cron (per App-Author konfigurierbar, typisch täglich) | Findet User mit abgelaufener Grace, ruft `EXT_USER_DATA.delete` pro Provider, anonymisiert User, sendet `sendDeletionExecutedEmail`-Callback | `read_job_runs` (success/fail), Hook-Errors als `errors[]` im Result |
|
|
134
|
+
| `run-export-jobs` | Cron + Event-getriggert | Findet pending Export-Jobs, ruft `runUserExport` → ZIP-Bau → Storage-Upload → Magic-Link-Token → `sendExportReadyEmail` | `read_job_runs`, `read_export_jobs.status` |
|
|
135
|
+
| `data-retention-cleanup` | Cron (in `data-retention`) | Cleant abgelaufene Rows pro Entity per `retention.keepFor` | `read_job_runs` |
|
|
136
|
+
| Token-Cleanup (implizit) | Per `compliance-profiles.userRights.exportDownloadTtl` | Worker-Job entfernt expired Magic-Link-Tokens + Storage-Binaries | `read_export_download_tokens` |
|
|
137
|
+
| Download-Attempt-Cleanup | Per Entity-Default 90d | Cleant `read_download_attempts` | — |
|
|
138
138
|
|
|
139
139
|
App-Author registriert die Cron-Trigger im run-config; `jobs`-Feature
|
|
140
140
|
persistiert Run-Tracking + Retry-Pfad.
|
|
@@ -18,16 +18,16 @@ defineFeature("tasks", (r) => {
|
|
|
18
18
|
r.useExtension(EXT_USER_DATA, "task", {
|
|
19
19
|
export: async (ctx) => ({
|
|
20
20
|
entity: "task",
|
|
21
|
-
rows: await ctx.db.select().from(tasksTable)
|
|
22
|
-
.where(eq(tasksTable.authorId, ctx.userId)),
|
|
21
|
+
rows: await ctx.db.select().from(tasksTable).where(eq(tasksTable.authorId, ctx.userId)),
|
|
23
22
|
}),
|
|
24
23
|
delete: async (ctx, strategy) => {
|
|
25
24
|
if (strategy === "anonymize") {
|
|
26
|
-
await ctx.db
|
|
25
|
+
await ctx.db
|
|
26
|
+
.update(tasksTable)
|
|
27
|
+
.set({ authorId: null })
|
|
27
28
|
.where(eq(tasksTable.authorId, ctx.userId));
|
|
28
29
|
} else {
|
|
29
|
-
await ctx.db.delete(tasksTable)
|
|
30
|
-
.where(eq(tasksTable.authorId, ctx.userId));
|
|
30
|
+
await ctx.db.delete(tasksTable).where(eq(tasksTable.authorId, ctx.userId));
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
});
|
|
@@ -42,15 +42,15 @@ Hook-Signaturen in `framework/src/engine/extensions/user-data.ts`:
|
|
|
42
42
|
|
|
43
43
|
## Endpoints
|
|
44
44
|
|
|
45
|
-
| Article
|
|
46
|
-
|
|
47
|
-
| Art. 15
|
|
48
|
-
| Art. 15+20 | `user-data-rights:write:request-export`
|
|
49
|
-
| Art. 17
|
|
50
|
-
| Art. 17
|
|
51
|
-
| Art. 18
|
|
52
|
-
| Art. 18
|
|
53
|
-
| Operator
|
|
45
|
+
| Article | Handler | Zweck |
|
|
46
|
+
| ---------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| Art. 15 | `user-data-rights:query:my-audit-log` | User sieht eigene Framework-Events (account-weit über alle Memberships). Domain-Entities ohne `ctx.events.append` erscheinen NICHT — nur im Export-Bundle. |
|
|
48
|
+
| Art. 15+20 | `user-data-rights:write:request-export` | Async Job → ZIP mit user-Profil + fileRefs + alle EXT_USER_DATA-Provider-Daten + signed Magic-Link |
|
|
49
|
+
| Art. 17 | `user-data-rights:write:request-deletion` | Soft-Delete mit Grace-Period, anschließend Cron anonymisiert User + cleant Domain-Entities |
|
|
50
|
+
| Art. 17 | `user-data-rights:write:cancel-deletion` | User widerruft seinen Forget-Request während der Grace |
|
|
51
|
+
| Art. 18 | `user-data-rights:write:restrict-account` | Auth-Middleware blockt Logins bis Lift |
|
|
52
|
+
| Art. 18 | `user-data-rights:write:lift-restriction` | Admin/SystemAdmin hebt Restriction auf |
|
|
53
|
+
| Operator | `user-data-rights:query:list-download-attempts` | DPO-Sicht auf invalid Download-Versuche (Brute-Force-Detection, Admin/SystemAdmin only) |
|
|
54
54
|
|
|
55
55
|
Plus 2 anonyme HTTP-Routes für Export-Download (Magic-Link-Pfad +
|
|
56
56
|
session-auth-Pfad), siehe `handlers/download-by-{token,job}.query.ts`.
|
|
@@ -77,30 +77,30 @@ weglassen wenn er Custom-Hooks registrieren will.
|
|
|
77
77
|
|
|
78
78
|
## Audit-Trail
|
|
79
79
|
|
|
80
|
-
| Tabelle
|
|
81
|
-
|
|
82
|
-
| `kumiko_events`
|
|
83
|
-
| `read_export_jobs`
|
|
84
|
-
| `read_export_download_tokens` | Magic-Link-Hash + TTL + lastUsed-Audit
|
|
85
|
-
| `read_download_attempts`
|
|
80
|
+
| Tabelle | Zweck | Retention |
|
|
81
|
+
| ----------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
82
|
+
| `kumiko_events` | Framework-Event-Store, Quelle für `my-audit-log` | per Domain-Policy |
|
|
83
|
+
| `read_export_jobs` | Async Export-Status (queued / done / failed) | per `compliance-profiles` |
|
|
84
|
+
| `read_export_download_tokens` | Magic-Link-Hash + TTL + lastUsed-Audit | per `compliance-profiles` (default `exportDownloadTtl`) |
|
|
85
|
+
| `read_download_attempts` | Invalid-Download-Versuche für DPO-Brute-Force-Detection | **90d hardDelete** (Entity-Default — schützt vor Disk-Bomb bei aktiven Angriffen) |
|
|
86
86
|
|
|
87
87
|
## Tests
|
|
88
88
|
|
|
89
89
|
18 Testdateien, 188 Tests, alle grün:
|
|
90
90
|
|
|
91
|
-
| Datei
|
|
92
|
-
|
|
93
|
-
| `audit-log.integration.test.ts`
|
|
94
|
-
| `cross-data-matrix.integration.test.ts`
|
|
95
|
-
| `download.integration.test.ts`
|
|
96
|
-
| `request-export.integration.test.ts`
|
|
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`
|
|
99
|
-
| `run-{export-jobs,forget-cleanup,user-export}.integration.test.ts`
|
|
100
|
-
| `policy-to-strategy.test.ts`
|
|
101
|
-
| `user-data-rights.integration.test.ts`
|
|
102
|
-
| `token-helpers.test.ts` + `zip-path.test.ts`
|
|
103
|
-
| `export-job-{idempotency,schema}.test.ts`
|
|
91
|
+
| Datei | Pinst |
|
|
92
|
+
| ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
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
|
+
| `policy-to-strategy.test.ts` | Retention.strategy → UserDataDeleteStrategy mapping |
|
|
101
|
+
| `user-data-rights.integration.test.ts` | Boot-Smoke + Feature-Meta |
|
|
102
|
+
| `token-helpers.test.ts` + `zip-path.test.ts` | Token-Hashing + Path-Traversal-Schutz |
|
|
103
|
+
| `export-job-{idempotency,schema}.test.ts` | Active-job-uniqueness + Schema-Constraints |
|
|
104
104
|
|
|
105
105
|
## Sample
|
|
106
106
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createTenantDb, type DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import {
|
|
3
|
+
type EscapeHatchAuditSink,
|
|
4
|
+
EXT_USER_DATA,
|
|
5
|
+
type TenantId,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import { createFallbackLogger, type Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
8
|
+
import {
|
|
9
|
+
createEscapeHatchReporter,
|
|
10
|
+
UNATTRIBUTED_ACTOR,
|
|
11
|
+
} from "@cosmicdrift/kumiko-framework/pipeline";
|
|
12
|
+
|
|
13
|
+
export function buildHookDb(
|
|
14
|
+
db: DbRunner,
|
|
15
|
+
tenantId: TenantId,
|
|
16
|
+
entry: { readonly entityName: string; readonly escapeHatchReason: string | undefined },
|
|
17
|
+
args: {
|
|
18
|
+
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
19
|
+
readonly escapeHatchAuditLog?: Logger;
|
|
20
|
+
readonly actor?: string;
|
|
21
|
+
},
|
|
22
|
+
) {
|
|
23
|
+
const report = createEscapeHatchReporter({
|
|
24
|
+
handler: `${EXT_USER_DATA}:${entry.entityName}`,
|
|
25
|
+
tenantId,
|
|
26
|
+
actor: args.actor ?? UNATTRIBUTED_ACTOR,
|
|
27
|
+
sink: args.escapeHatchAuditSink,
|
|
28
|
+
log: args.escapeHatchAuditLog ?? createFallbackLogger("user-data-rights"),
|
|
29
|
+
});
|
|
30
|
+
return createTenantDb({
|
|
31
|
+
db,
|
|
32
|
+
tenantId,
|
|
33
|
+
mode: "tenant",
|
|
34
|
+
grants: {
|
|
35
|
+
unsafeRaw:
|
|
36
|
+
entry.escapeHatchReason !== undefined ? { reason: entry.escapeHatchReason } : undefined,
|
|
37
|
+
report,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
|
|
24
24
|
import { createEventStoreExecutor, fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
25
25
|
import { defineWriteHandler, type SaveContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
26
|
-
import type { WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
27
26
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
28
27
|
import * as z from "zod";
|
|
28
|
+
import { isUniqueConstraintFailure } from "../../shared/index.ts";
|
|
29
29
|
import {
|
|
30
30
|
ACTIVE_JOB_CONSTRAINT,
|
|
31
31
|
EXPORT_JOB_STATUS,
|
|
@@ -40,23 +40,6 @@ const crud = createEventStoreExecutor(exportJobsTable, exportJobEntity, {
|
|
|
40
40
|
const REQUEST_EXPORT_REASON =
|
|
41
41
|
"export jobs are keyed by userId across all of the user's tenant memberships, not the caller's current tenant";
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* `createEventStoreExecutor.create` catches 23505 internally and returns
|
|
45
|
-
* `WriteFailure(UniqueViolationError)` rather than throwing. Check the
|
|
46
|
-
* failure code and constraintName to distinguish the app-side and race
|
|
47
|
-
* paths; propagate other failures (validation, version conflicts, ...)
|
|
48
|
-
* unchanged.
|
|
49
|
-
*/
|
|
50
|
-
function isActiveJobConflict(failure: WriteFailure): boolean {
|
|
51
|
-
const error = failure.error as {
|
|
52
|
-
code?: string;
|
|
53
|
-
details?: { constraintName?: string };
|
|
54
|
-
}; // @cast-boundary error-details
|
|
55
|
-
return (
|
|
56
|
-
error.code === "unique_violation" && error.details?.constraintName === ACTIVE_JOB_CONSTRAINT
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
43
|
export const requestExportWrite = defineWriteHandler({
|
|
61
44
|
name: "request-export",
|
|
62
45
|
schema: z.object({}),
|
|
@@ -115,7 +98,7 @@ export const requestExportWrite = defineWriteHandler({
|
|
|
115
98
|
// crud.create, causing UniqueViolationError for our constraint. Re-fetch
|
|
116
99
|
// and return the existing job with isExisting=true. Propagate other
|
|
117
100
|
// failures (validation, version conflicts, ...) unchanged.
|
|
118
|
-
if (
|
|
101
|
+
if (isUniqueConstraintFailure(result, ACTIVE_JOB_CONSTRAINT)) {
|
|
119
102
|
const winner = await findActiveJob(exportJobRunner, userId);
|
|
120
103
|
if (!winner) {
|
|
121
104
|
// This should be impossible: a constraint violation without an existing
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type FeatureRegistrar,
|
|
3
|
-
type NotificationLocalizedTemplate,
|
|
4
3
|
QnTypes,
|
|
5
4
|
qn,
|
|
6
5
|
type TemplateVariables,
|
|
7
6
|
templateVariable,
|
|
8
7
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
-
import {
|
|
10
|
-
hasMailTranslationKey,
|
|
11
|
-
listMailLocales,
|
|
12
|
-
mailT,
|
|
13
|
-
resolveMailLocale,
|
|
14
|
-
} from "@cosmicdrift/kumiko-framework/i18n";
|
|
8
|
+
import { mailT } from "@cosmicdrift/kumiko-framework/i18n";
|
|
15
9
|
import { escapeHtml } from "@cosmicdrift/kumiko-renderer/headless";
|
|
16
|
-
import { mailParagraph } from "../shared/index.ts";
|
|
10
|
+
import { createMailLocaleTemplates, mailParagraph } from "../shared/index.ts";
|
|
17
11
|
import { USER_DATA_RIGHTS_FEATURE } from "./web/constants.ts";
|
|
18
12
|
|
|
19
13
|
const ALL_GDPR_MAIL_KINDS = [
|
|
@@ -49,18 +43,14 @@ function subjectKey(kind: GdprMailKind): string {
|
|
|
49
43
|
return `gdpr.mail.${GDPR_MAIL_KINDS[kind].translationKey}.subject`;
|
|
50
44
|
}
|
|
51
45
|
|
|
52
|
-
const
|
|
53
|
-
ALL_GDPR_MAIL_KINDS
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
export
|
|
60
|
-
const locale = resolveMailLocale(requestedLocale);
|
|
61
|
-
const key = SUBJECT_KEY_BY_TYPE.get(notificationType);
|
|
62
|
-
return key !== undefined && hasMailTranslationKey(locale, key) ? locale : "en";
|
|
63
|
-
}
|
|
46
|
+
const mailLocaleTemplates = createMailLocaleTemplates({
|
|
47
|
+
kinds: ALL_GDPR_MAIL_KINDS,
|
|
48
|
+
notificationTypes: GdprMailNotificationTypes,
|
|
49
|
+
subjectKey,
|
|
50
|
+
bodyFor: (kind, locale) => mailBody(kind, locale),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const gdprMailTemplateLocale = mailLocaleTemplates.templateLocale;
|
|
64
54
|
|
|
65
55
|
const appName = () => templateVariable.text({ required: true });
|
|
66
56
|
|
|
@@ -119,33 +109,7 @@ function mailBody(kind: GdprMailKind, locale: string): string {
|
|
|
119
109
|
}
|
|
120
110
|
}
|
|
121
111
|
|
|
122
|
-
|
|
123
|
-
// registered its mail translations. English always ships; another locale only
|
|
124
|
-
// gets a template when it has its own subject, so a missing translation never
|
|
125
|
-
// silently becomes an English mail in that locale's slot.
|
|
126
|
-
export function gdprMailLocalizedTemplate(kind: GdprMailKind): () => NotificationLocalizedTemplate {
|
|
127
|
-
const kindSubjectKey = subjectKey(kind);
|
|
128
|
-
return () => {
|
|
129
|
-
const locales = [
|
|
130
|
-
"en",
|
|
131
|
-
...listMailLocales().filter(
|
|
132
|
-
(locale) => locale !== "en" && hasMailTranslationKey(locale, kindSubjectKey),
|
|
133
|
-
),
|
|
134
|
-
];
|
|
135
|
-
return Object.fromEntries(
|
|
136
|
-
locales.map((locale) => [
|
|
137
|
-
locale,
|
|
138
|
-
{
|
|
139
|
-
body: mailBody(kind, locale),
|
|
140
|
-
meta: {
|
|
141
|
-
subject: mailT(locale, kindSubjectKey, { app: "{{appName}}" }),
|
|
142
|
-
category: "transactional" as const,
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
]),
|
|
146
|
-
);
|
|
147
|
-
};
|
|
148
|
-
}
|
|
112
|
+
export const gdprMailLocalizedTemplate = mailLocaleTemplates.localizedTemplate;
|
|
149
113
|
|
|
150
114
|
export function registerGdprMails(r: FeatureRegistrar): void {
|
|
151
115
|
for (const kind of ALL_GDPR_MAIL_KINDS) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
2
2
|
// Forget-cleanup runner — pure-function pipeline.
|
|
3
3
|
//
|
|
4
4
|
// After the grace period, this runner processes users in DeletionRequested
|
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
subjectIdToKey,
|
|
35
35
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
36
36
|
import {
|
|
37
|
-
createTenantDb,
|
|
38
37
|
type DbRunner,
|
|
39
38
|
entityEventName,
|
|
40
39
|
executeRawQuery,
|
|
@@ -57,10 +56,6 @@ import {
|
|
|
57
56
|
type UserDataStorageProvider,
|
|
58
57
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
59
58
|
import { runInSystemRowLevelSecurityScope } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
|
|
60
|
-
import {
|
|
61
|
-
createEscapeHatchReporter,
|
|
62
|
-
UNATTRIBUTED_ACTOR,
|
|
63
|
-
} from "@cosmicdrift/kumiko-framework/pipeline";
|
|
64
59
|
import {
|
|
65
60
|
purgeSearchDocumentsForSubject,
|
|
66
61
|
type SearchAdapter,
|
|
@@ -78,6 +73,7 @@ import {
|
|
|
78
73
|
} from "../shared/index.ts";
|
|
79
74
|
import { tenantMembershipsTable } from "../tenant/index.ts";
|
|
80
75
|
import { USER_STATUS, userTable } from "../user/index.ts";
|
|
76
|
+
import { buildHookDb } from "./build-hook-db.ts";
|
|
81
77
|
import { selectUsersDueForForgetCleanup } from "./db/queries/forget-cleanup.ts";
|
|
82
78
|
import { updateUserLifecycle } from "./lib/update-user-lifecycle.ts";
|
|
83
79
|
|
|
@@ -205,35 +201,6 @@ interface HookEntry {
|
|
|
205
201
|
readonly escapeHatchReason: string | undefined;
|
|
206
202
|
}
|
|
207
203
|
|
|
208
|
-
function buildHookDb(
|
|
209
|
-
db: DbRunner,
|
|
210
|
-
tenantId: TenantId,
|
|
211
|
-
entry: HookEntry,
|
|
212
|
-
args: {
|
|
213
|
-
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
214
|
-
readonly escapeHatchAuditLog?: Logger;
|
|
215
|
-
readonly actor?: string;
|
|
216
|
-
},
|
|
217
|
-
) {
|
|
218
|
-
const report = createEscapeHatchReporter({
|
|
219
|
-
handler: `${EXT_USER_DATA}:${entry.entityName}`,
|
|
220
|
-
tenantId,
|
|
221
|
-
actor: args.actor ?? UNATTRIBUTED_ACTOR,
|
|
222
|
-
sink: args.escapeHatchAuditSink,
|
|
223
|
-
log: args.escapeHatchAuditLog ?? createFallbackLogger("user-data-rights"),
|
|
224
|
-
});
|
|
225
|
-
return createTenantDb({
|
|
226
|
-
db,
|
|
227
|
-
tenantId,
|
|
228
|
-
mode: "tenant",
|
|
229
|
-
grants: {
|
|
230
|
-
unsafeRaw:
|
|
231
|
-
entry.escapeHatchReason !== undefined ? { reason: entry.escapeHatchReason } : undefined,
|
|
232
|
-
report,
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
204
|
// EXT_USER_DATA delete-hooks default here; a hook that redacts data keyed on an
|
|
238
205
|
// owner column it doesn't own must register a lower order so it runs BEFORE any
|
|
239
206
|
// hook that nulls that column. See custom-fields wire-user-data-rights.ts.
|