@cosmicdrift/kumiko-bundled-features 0.337.1 → 0.338.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/dist/agent-tools/tool-catalog.js +37 -17
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +1 -0
- package/dist/auth-email-password/handlers/invite-accept.write.js +1 -0
- package/dist/auth-email-password/handlers/invite-info.query.js +1 -0
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +1 -0
- package/dist/auth-email-password/handlers/signup-confirm.write.js +1 -0
- package/dist/auth-email-password/handlers/signup-request.write.js +1 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -0
- package/dist/billing-foundation/aggregate-id.d.ts +2 -0
- package/dist/billing-foundation/aggregate-id.js +8 -0
- package/dist/billing-foundation/checkout-core.d.ts +3 -0
- package/dist/billing-foundation/checkout-core.js +3 -0
- package/dist/billing-foundation/constants.d.ts +31 -0
- package/dist/billing-foundation/constants.js +35 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +21 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +125 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +32 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +88 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.d.ts +5 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.js +17 -0
- package/dist/billing-foundation/consumer-protection/consent-text.d.ts +11 -0
- package/dist/billing-foundation/consumer-protection/consent-text.js +28 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.d.ts +15 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +149 -0
- package/dist/billing-foundation/consumer-protection/register-consent.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-consent.js +63 -0
- package/dist/billing-foundation/consumer-protection/register-termination.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-termination.js +12 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +43 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.js +76 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.d.ts +8 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.js +239 -0
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +51 -0
- package/dist/billing-foundation/consumer-protection/termination-record.js +162 -0
- package/dist/billing-foundation/consumer-protection/termination-request.d.ts +45 -0
- package/dist/billing-foundation/consumer-protection/termination-request.js +263 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +62 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.js +168 -0
- package/dist/billing-foundation/events.d.ts +86 -2
- package/dist/billing-foundation/events.js +76 -2
- package/dist/billing-foundation/feature.js +6 -2
- package/dist/billing-foundation/handlers/billing-plans.query.js +19 -1
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +33 -2
- package/dist/billing-foundation/handlers/process-event.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/process-event.write.js +43 -15
- package/dist/billing-foundation/handlers/process-payment-event.write.d.ts +2 -1
- package/dist/billing-foundation/handlers/process-payment-event.write.js +6 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +15 -1
- package/dist/billing-foundation/handlers/sync-subscription.write.js +0 -1
- package/dist/billing-foundation/i18n.js +58 -0
- package/dist/billing-foundation/index.d.ts +3 -2
- package/dist/billing-foundation/index.js +2 -1
- package/dist/billing-foundation/types.d.ts +49 -9
- package/dist/billing-foundation/validate-options.js +47 -10
- package/dist/billing-foundation/web/billing-plans-panel.js +40 -3
- package/dist/billing-foundation/web/cancel-contract-dialog.d.ts +7 -0
- package/dist/billing-foundation/web/cancel-contract-dialog.js +57 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +28 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.js +38 -0
- package/dist/billing-foundation/webhook-handler.js +5 -2
- package/dist/cap-counter/with-cap-enforcement.js +7 -3
- package/dist/cap-overview/access.d.ts +2 -1
- package/dist/cap-overview/access.js +26 -5
- package/dist/cap-overview/feature.d.ts +4 -0
- package/dist/cap-overview/feature.js +8 -6
- package/dist/cap-overview/handlers/caps-usage.query.d.ts +1 -1
- package/dist/cap-overview/handlers/caps-usage.query.js +6 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +12 -7
- package/dist/cap-overview/index.d.ts +2 -2
- package/dist/cap-overview/index.js +2 -2
- package/dist/cap-overview/screens.d.ts +1 -1
- package/dist/cap-overview/screens.js +17 -16
- package/dist/config/handlers/set.write.js +18 -2
- package/dist/crypto-shredding/feature.js +3 -2
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +24 -13
- package/dist/custom-fields/feature.js +9 -6
- package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -0
- package/dist/custom-fields/handlers/define-tenant-field.write.js +1 -0
- package/dist/custom-fields/handlers/set-custom-field.write.js +1 -0
- package/dist/delivery/delivery-service.js +9 -7
- package/dist/document-ingest-foundation/feature.js +3 -2
- package/dist/form-draft/handlers/discard.write.js +1 -0
- package/dist/form-draft/handlers/save.write.js +1 -0
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -0
- package/dist/managed-pages/handlers/set.write.js +1 -0
- package/dist/notes-history/executor.d.ts +2 -0
- package/dist/notes-history/executor.js +9 -1
- package/dist/notes-history/feature.d.ts +2 -3
- package/dist/notes-history/feature.js +2 -1
- package/dist/notes-history/handlers/add-note.write.d.ts +2 -1
- package/dist/notes-history/handlers/add-note.write.js +20 -3
- package/dist/notes-history-user-data/hooks.js +26 -8
- package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -0
- package/dist/subscription-mollie/plugin-methods.js +1 -0
- package/dist/subscription-mollie/verify-webhook.js +5 -3
- package/dist/subscription-stripe/plugin-methods.d.ts +3 -1
- package/dist/subscription-stripe/plugin-methods.js +78 -13
- package/dist/subscription-stripe/verify-webhook.js +7 -3
- package/dist/tags/executor.d.ts +2 -0
- package/dist/tags/executor.js +9 -1
- package/dist/tags/feature.d.ts +4 -3
- package/dist/tags/feature.js +4 -2
- package/dist/tags/handlers/assign-tag.write.d.ts +2 -1
- package/dist/tags/handlers/assign-tag.write.js +4 -4
- package/dist/tags/handlers/remove-tag.write.d.ts +2 -1
- package/dist/tags/handlers/remove-tag.write.js +3 -3
- package/dist/tenant-handover/handlers/claim.write.js +1 -0
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +1 -0
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +1 -0
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +1 -0
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -0
- package/dist/user/handlers/detail.query.js +1 -0
- package/dist/user/handlers/list.query.js +1 -0
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +1 -0
- package/dist/user-data-rights/handlers/download-attempt-list.query.js +1 -0
- package/dist/user-data-rights/handlers/download-by-job.query.js +1 -0
- package/dist/user-data-rights/handlers/download-by-token.query.js +1 -0
- package/dist/user-data-rights/handlers/export-status.query.js +1 -0
- package/dist/user-data-rights/handlers/lift-restriction.write.js +1 -0
- package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -0
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -0
- package/dist/user-data-rights/handlers/request-export.write.js +1 -0
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +1 -0
- package/dist/user-data-rights/run-forget-cleanup.js +30 -15
- package/package.json +9 -9
- package/src/agent-tools/changes.json +14 -0
- package/src/billing-foundation/changes.json +49 -0
- package/src/cap-counter/changes.json +7 -0
- package/src/cap-overview/changes.json +14 -0
- package/src/config/changes.json +7 -0
- package/src/crypto-shredding/changes.json +13 -0
- package/src/custom-fields/changes.json +7 -0
- package/src/delivery/changes.json +7 -0
- package/src/notes-history/changes.json +7 -0
- package/src/subscription-mollie/changes.json +13 -0
- package/src/subscription-stripe/changes.json +13 -0
- package/src/tags/changes.json +7 -0
- package/src/user-data-rights/changes.json +6 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// muss `customerSubscriptions.create` selbst aufrufen). Wir machen
|
|
14
14
|
// das im Plugin (idempotent via list-check), damit Foundation einen
|
|
15
15
|
// Created-Event mit subscription-id bekommt.
|
|
16
|
-
import { SubscriptionEventTypes, SubscriptionStatuses, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
16
|
+
import { parseProviderConsentId, SubscriptionEventTypes, SubscriptionStatuses, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
17
17
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
18
18
|
import { Temporal } from "temporal-polyfill";
|
|
19
19
|
import { MOLLIE_PROVIDER_NAME } from "./constants.js";
|
|
@@ -85,17 +85,18 @@ export function verifyAndParseMollieWebhook(client, options) {
|
|
|
85
85
|
throw new Error(`subscription-mollie: subscription ${subscription.id} has neither nextPaymentDate nor startDate`);
|
|
86
86
|
}
|
|
87
87
|
const currentPeriodEnd = mollieDateStringToInstantIso(periodEndSource);
|
|
88
|
+
const consentId = parseProviderConsentId(metadata["consentId"]);
|
|
88
89
|
return {
|
|
89
90
|
providerEventId: id,
|
|
90
91
|
providerName: MOLLIE_PROVIDER_NAME,
|
|
91
92
|
type,
|
|
92
93
|
tenantId,
|
|
94
|
+
...(consentId !== undefined && { consentId }),
|
|
93
95
|
providerCustomerId: subscription.customerId,
|
|
94
96
|
providerSubscriptionId: subscription.id,
|
|
95
97
|
status,
|
|
96
98
|
tier,
|
|
97
99
|
currentPeriodEnd,
|
|
98
|
-
rawPayload: JSON.stringify({ webhookId: id, subscription, triggerPayment }),
|
|
99
100
|
};
|
|
100
101
|
};
|
|
101
102
|
}
|
|
@@ -112,6 +113,7 @@ async function ensureSubscriptionForMandate(client, options, payment) {
|
|
|
112
113
|
const paymentMetadata = payment.metadata ?? {}; // @cast-boundary engine-bridge
|
|
113
114
|
const tenantId = paymentMetadata["tenantId"];
|
|
114
115
|
const priceId = paymentMetadata["priceId"];
|
|
116
|
+
const consentId = parseProviderConsentId(paymentMetadata["consentId"]);
|
|
115
117
|
if (!tenantId || !priceId)
|
|
116
118
|
return null;
|
|
117
119
|
const priceCfg = options.priceToConfig[priceId];
|
|
@@ -130,7 +132,7 @@ async function ensureSubscriptionForMandate(client, options, payment) {
|
|
|
130
132
|
amount: { currency: priceCfg.amountCurrency, value: priceCfg.amountValue },
|
|
131
133
|
interval: priceCfg.interval,
|
|
132
134
|
description: priceCfg.description,
|
|
133
|
-
metadata: { tenantId, priceId },
|
|
135
|
+
metadata: { tenantId, priceId, ...(consentId && { consentId }) },
|
|
134
136
|
});
|
|
135
137
|
}
|
|
136
138
|
// =============================================================================
|
|
@@ -8,6 +8,7 @@ export type StripeCheckoutSessionRuntimeOptions = {
|
|
|
8
8
|
* Never applied in mode:"subscription" — Stripe rejects invoice_creation there. */
|
|
9
9
|
readonly paymentInvoiceCreation?: boolean;
|
|
10
10
|
};
|
|
11
|
+
export declare function mapAppLocaleToStripeLocale(locale: string): Stripe.Checkout.SessionCreateParams.Locale;
|
|
11
12
|
export declare function createStripeCheckoutSession(runtime: StripeCtxRuntime, { paymentInvoiceCreation }?: StripeCheckoutSessionRuntimeOptions): (ctx: HandlerContext, options: StripeCheckoutOptions) => Promise<{
|
|
12
13
|
url: string;
|
|
13
14
|
}>;
|
|
@@ -15,7 +16,8 @@ export type StripePortalOptions = Parameters<NonNullable<SubscriptionProviderPlu
|
|
|
15
16
|
export declare function createStripePortalSession(runtime: StripeCtxRuntime): (ctx: HandlerContext, options: StripePortalOptions) => Promise<{
|
|
16
17
|
url: string;
|
|
17
18
|
}>;
|
|
18
|
-
export
|
|
19
|
+
export type StripeCancelOptions = Parameters<NonNullable<SubscriptionProviderPlugin["cancelSubscription"]>>[1];
|
|
20
|
+
export declare function createStripeCancelSubscription(runtime: StripeCtxRuntime): (ctx: HandlerContext, options: StripeCancelOptions) => Promise<void>;
|
|
19
21
|
export type StripeRetrieveSubscriptionOptions = {
|
|
20
22
|
readonly priceToTier: Readonly<Record<string, string>>;
|
|
21
23
|
};
|
|
@@ -15,10 +15,65 @@
|
|
|
15
15
|
// Contract erweitert (z.B. neuer optionaler Field), bemerkt der
|
|
16
16
|
// Stripe-Plugin das beim TS-Compile, nicht erst zur Laufzeit.
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
|
-
import { KNOWN_RECURRING_INTERVALS, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
18
|
+
import { KNOWN_RECURRING_INTERVALS, SubscriptionCancelTimings, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
19
19
|
import { ConflictError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
20
20
|
import Stripe from "stripe";
|
|
21
21
|
import { isResourceMissingStripeError, mapStripeSubscriptionState } from "./verify-webhook.js";
|
|
22
|
+
// Stripe's own Locale union also accepts arbitrary strings, so the known set
|
|
23
|
+
// is spelled out here to pick a value Stripe actually renders.
|
|
24
|
+
const STRIPE_CHECKOUT_LOCALES = [
|
|
25
|
+
"bg",
|
|
26
|
+
"cs",
|
|
27
|
+
"da",
|
|
28
|
+
"de",
|
|
29
|
+
"el",
|
|
30
|
+
"en",
|
|
31
|
+
"en-GB",
|
|
32
|
+
"es",
|
|
33
|
+
"es-419",
|
|
34
|
+
"et",
|
|
35
|
+
"fi",
|
|
36
|
+
"fil",
|
|
37
|
+
"fr",
|
|
38
|
+
"fr-CA",
|
|
39
|
+
"hr",
|
|
40
|
+
"hu",
|
|
41
|
+
"id",
|
|
42
|
+
"it",
|
|
43
|
+
"ja",
|
|
44
|
+
"ko",
|
|
45
|
+
"lt",
|
|
46
|
+
"lv",
|
|
47
|
+
"ms",
|
|
48
|
+
"mt",
|
|
49
|
+
"nb",
|
|
50
|
+
"nl",
|
|
51
|
+
"pl",
|
|
52
|
+
"pt",
|
|
53
|
+
"pt-BR",
|
|
54
|
+
"ro",
|
|
55
|
+
"ru",
|
|
56
|
+
"sk",
|
|
57
|
+
"sl",
|
|
58
|
+
"sv",
|
|
59
|
+
"th",
|
|
60
|
+
"tr",
|
|
61
|
+
"vi",
|
|
62
|
+
"zh",
|
|
63
|
+
"zh-HK",
|
|
64
|
+
"zh-TW",
|
|
65
|
+
];
|
|
66
|
+
// Stripe rejects custom_text.submit.message above 1200 characters.
|
|
67
|
+
const STRIPE_CUSTOM_TEXT_MAX_LENGTH = 1200;
|
|
68
|
+
export function mapAppLocaleToStripeLocale(locale) {
|
|
69
|
+
const normalized = locale.replace("_", "-");
|
|
70
|
+
const knownLocales = STRIPE_CHECKOUT_LOCALES;
|
|
71
|
+
const exact = knownLocales.find((known) => known.toLowerCase() === normalized.toLowerCase());
|
|
72
|
+
if (exact)
|
|
73
|
+
return exact;
|
|
74
|
+
const language = normalized.split("-")[0]?.toLowerCase();
|
|
75
|
+
return knownLocales.find((known) => known === language) ?? "auto";
|
|
76
|
+
}
|
|
22
77
|
function isUnknownCustomerStripeError(error) {
|
|
23
78
|
return (isResourceMissingStripeError(error) &&
|
|
24
79
|
error instanceof Stripe.errors.StripeInvalidRequestError &&
|
|
@@ -34,11 +89,22 @@ export function createStripeCheckoutSession(runtime, { paymentInvoiceCreation =
|
|
|
34
89
|
await runtime.assertBillingLive(ctx);
|
|
35
90
|
const stripe = await runtime.clientForCtx(ctx);
|
|
36
91
|
const mode = options.mode ?? "subscription";
|
|
92
|
+
const metadata = {
|
|
93
|
+
tenantId: options.tenantId,
|
|
94
|
+
...(options.consentId && { consentId: options.consentId }),
|
|
95
|
+
};
|
|
37
96
|
const sessionParams = (providerCustomerId) => ({
|
|
38
97
|
mode,
|
|
39
98
|
line_items: [{ price: options.priceId, quantity: 1 }],
|
|
40
99
|
success_url: options.successUrl,
|
|
41
100
|
cancel_url: options.cancelUrl,
|
|
101
|
+
submit_type: "pay",
|
|
102
|
+
...(options.locale && { locale: mapAppLocaleToStripeLocale(options.locale) }),
|
|
103
|
+
...(options.submitMessage && {
|
|
104
|
+
custom_text: {
|
|
105
|
+
submit: { message: options.submitMessage.slice(0, STRIPE_CUSTOM_TEXT_MAX_LENGTH) },
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
42
108
|
// subscription_data is subscription-mode-only (Stripe rejects it in
|
|
43
109
|
// payment-mode) — the subsequent webhook reads metadata.tenantId off
|
|
44
110
|
// the subscription it creates. payment-mode has no subscription, so
|
|
@@ -47,9 +113,9 @@ export function createStripeCheckoutSession(runtime, { paymentInvoiceCreation =
|
|
|
47
113
|
// rejects it in subscription-mode, which invoices via the subscription
|
|
48
114
|
// itself.
|
|
49
115
|
...(mode === "subscription"
|
|
50
|
-
? { subscription_data: { metadata
|
|
116
|
+
? { subscription_data: { metadata } }
|
|
51
117
|
: {
|
|
52
|
-
payment_intent_data: { metadata
|
|
118
|
+
payment_intent_data: { metadata },
|
|
53
119
|
invoice_creation: { enabled: paymentInvoiceCreation },
|
|
54
120
|
// Without a customer, Stripe's default "if_required" makes a guest
|
|
55
121
|
// checkout when invoice_creation is off; the webhook then has no
|
|
@@ -88,17 +154,17 @@ export function createStripePortalSession(runtime) {
|
|
|
88
154
|
return { url: session.url };
|
|
89
155
|
};
|
|
90
156
|
}
|
|
91
|
-
// =============================================================================
|
|
92
|
-
// cancelSubscription
|
|
93
|
-
// =============================================================================
|
|
94
|
-
//
|
|
95
|
-
// Stripe sendet danach `customer.subscription.deleted`-webhook → der
|
|
96
|
-
// state-update läuft über den normalen webhook-pfad. Diese function
|
|
97
|
-
// triggert nur die API-Cancellation.
|
|
98
157
|
export function createStripeCancelSubscription(runtime) {
|
|
99
|
-
return async (ctx,
|
|
158
|
+
return async (ctx, options) => {
|
|
100
159
|
const stripe = await runtime.clientForCtx(ctx);
|
|
101
|
-
|
|
160
|
+
if (options.when === SubscriptionCancelTimings.periodEnd) {
|
|
161
|
+
await stripe.subscriptions.update(options.providerSubscriptionId, {
|
|
162
|
+
cancel_at_period_end: true,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
await stripe.subscriptions.cancel(options.providerSubscriptionId);
|
|
167
|
+
}
|
|
102
168
|
};
|
|
103
169
|
}
|
|
104
170
|
export function createStripeRetrieveSubscription(runtime, { priceToTier }) {
|
|
@@ -122,7 +188,6 @@ export function createStripeRetrieveSubscription(runtime, { priceToTier }) {
|
|
|
122
188
|
: subscription.customer.id,
|
|
123
189
|
providerSubscriptionId: subscription.id,
|
|
124
190
|
...state,
|
|
125
|
-
rawPayload: JSON.stringify(subscription),
|
|
126
191
|
};
|
|
127
192
|
};
|
|
128
193
|
}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// status/tier/period-end mapping. resource_missing (deleted between webhook
|
|
32
32
|
// and retrieve) returns null; any other Stripe error becomes 503 so Stripe
|
|
33
33
|
// redelivers.
|
|
34
|
-
import { BillingEventKinds, SubscriptionEventTypes, SubscriptionStatuses, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
34
|
+
import { BillingEventKinds, parseProviderConsentId, SubscriptionEventTypes, SubscriptionStatuses, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
35
35
|
import { ExtraRouteRejection } from "@cosmicdrift/kumiko-framework/api";
|
|
36
36
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
37
37
|
import Stripe from "stripe";
|
|
@@ -116,15 +116,16 @@ export function verifyAndParseStripeWebhook(runtime, options) {
|
|
|
116
116
|
if (!state) {
|
|
117
117
|
return null;
|
|
118
118
|
}
|
|
119
|
+
const consentId = parseProviderConsentId(sub.metadata?.["consentId"]);
|
|
119
120
|
return {
|
|
120
121
|
providerEventId: event.id,
|
|
121
122
|
providerName: STRIPE_PROVIDER_NAME,
|
|
122
123
|
type: normalizedType,
|
|
123
124
|
tenantId,
|
|
125
|
+
...(consentId !== undefined && { consentId }),
|
|
124
126
|
providerCustomerId: typeof sub.customer === "string" ? sub.customer : sub.customer.id,
|
|
125
127
|
providerSubscriptionId: sub.id,
|
|
126
128
|
...state,
|
|
127
|
-
rawPayload: JSON.stringify(event),
|
|
128
129
|
};
|
|
129
130
|
};
|
|
130
131
|
}
|
|
@@ -269,6 +270,7 @@ function dropPaidSession(event, session, reason) {
|
|
|
269
270
|
}
|
|
270
271
|
/** Parses a checkout.session.completed / .async_payment_succeeded event into
|
|
271
272
|
* a PaymentEvent, or null if it isn't a paid one-off-payment session. */
|
|
273
|
+
// kumiko-lint-ignore complexity-budget optional-field spreads over the session, one branch per optional provider field
|
|
272
274
|
async function parsePaymentEvent(event, stripe) {
|
|
273
275
|
const session = event.data.object; // @cast-boundary engine-bridge
|
|
274
276
|
// mode !== "payment" excludes subscription-checkout sessions (same event types);
|
|
@@ -326,13 +328,15 @@ async function parsePaymentEvent(event, stripe) {
|
|
|
326
328
|
if (!providerCustomerId) {
|
|
327
329
|
return dropPaidSession(event, session, "missing_customer");
|
|
328
330
|
}
|
|
331
|
+
const consentId = parseProviderConsentId(paymentIntent.metadata?.["consentId"]);
|
|
329
332
|
return {
|
|
330
333
|
kind: BillingEventKinds.payment,
|
|
331
334
|
providerEventId: event.id,
|
|
332
335
|
providerName: STRIPE_PROVIDER_NAME,
|
|
333
336
|
tenantId,
|
|
337
|
+
...(consentId !== undefined && { consentId }),
|
|
338
|
+
providerCheckoutId: session.id,
|
|
334
339
|
providerCustomerId,
|
|
335
340
|
priceId,
|
|
336
|
-
rawPayload: JSON.stringify(event),
|
|
337
341
|
};
|
|
338
342
|
}
|
package/dist/tags/executor.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
import { type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
2
|
export declare const tagExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor;
|
|
2
3
|
export declare const tagAssignmentExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor;
|
|
4
|
+
export declare function createTagAssignmentExecutor(ownership: EntityDefinition["access"] | undefined): typeof tagAssignmentExecutor;
|
package/dist/tags/executor.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { createEntityExecutor } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { tagAssignmentEntity, tagEntity } from "./entity.js";
|
|
2
|
+
import { createTagAssignmentEntity, tagAssignmentEntity, tagEntity } from "./entity.js";
|
|
3
3
|
// Shared executors for the tag + tag-assignment write-handlers.
|
|
4
4
|
// createEntityExecutor is side-effect-free; instantiating once keeps the
|
|
5
5
|
// table+executor pair in one place instead of rebuilding it per handler module.
|
|
6
6
|
export const { executor: tagExecutor } = createEntityExecutor("tag", tagEntity);
|
|
7
7
|
export const { executor: tagAssignmentExecutor } = createEntityExecutor("tag-assignment", tagAssignmentEntity);
|
|
8
|
+
// Carries only `ownership.write`: the executor enforces entity.access.write on
|
|
9
|
+
// create/restore/delete, while a read rule would hide rows from the handlers'
|
|
10
|
+
// own idempotency reads and the delete-tag cascade.
|
|
11
|
+
export function createTagAssignmentExecutor(ownership) {
|
|
12
|
+
if (ownership?.write === undefined)
|
|
13
|
+
return tagAssignmentExecutor;
|
|
14
|
+
return createEntityExecutor("tag-assignment", createTagAssignmentEntity({ write: ownership.write })).executor;
|
|
15
|
+
}
|
package/dist/tags/feature.d.ts
CHANGED
|
@@ -28,9 +28,10 @@ export type TagsFeatureOptions = {
|
|
|
28
28
|
* `tag-assignment`; the `tag` catalog stays tenant-wide by design.
|
|
29
29
|
*
|
|
30
30
|
* `ownership.write` is separate and does NOT affect list/read. It's
|
|
31
|
-
* consulted
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* consulted on every assignment write: assign-tag (create/restore),
|
|
32
|
+
* remove-tag (delete) and the framework's generic forget path. The
|
|
33
|
+
* delete-tag cascade stays ungated, since deleting a catalog tag detaches
|
|
34
|
+
* it everywhere. A `from()` rule there also gates GDPR erasure
|
|
34
35
|
* (`forget`): if the rule's role map doesn't cover whatever role the
|
|
35
36
|
* erasure/retention pipeline runs as, `forget` denies instead of
|
|
36
37
|
* crypto-shredding — a silent Art.17 failure, not a thrown error. Make
|
package/dist/tags/feature.js
CHANGED
|
@@ -21,6 +21,7 @@ import { defineEntityDetailHandler, defineEntityListHandler, defineEntityUpdateH
|
|
|
21
21
|
import { hasWhereRule } from "../shared/index.js";
|
|
22
22
|
import { DEFAULT_TAG_ACCESS, TAGS_FEATURE_NAME } from "./constants.js";
|
|
23
23
|
import { createTagAssignmentEntity, tagEntity } from "./entity.js";
|
|
24
|
+
import { createTagAssignmentExecutor } from "./executor.js";
|
|
24
25
|
import { createAssignTagHandler } from "./handlers/assign-tag.write.js";
|
|
25
26
|
import { createCreateTagHandler } from "./handlers/create-tag.write.js";
|
|
26
27
|
import { createDeleteTagHandler } from "./handlers/delete-tag.write.js";
|
|
@@ -45,8 +46,9 @@ function registerTags(r, access, toggleable, ownership, parents) {
|
|
|
45
46
|
r.writeHandler(createCreateTagHandler(access));
|
|
46
47
|
r.writeHandler(createUpdateTagHandler(access));
|
|
47
48
|
r.writeHandler(createDeleteTagHandler(access));
|
|
48
|
-
|
|
49
|
-
r.writeHandler(
|
|
49
|
+
const assignmentExecutor = createTagAssignmentExecutor(ownership);
|
|
50
|
+
r.writeHandler(createAssignTagHandler(access, assignmentExecutor));
|
|
51
|
+
r.writeHandler(createRemoveTagHandler(access, assignmentExecutor));
|
|
50
52
|
// Convention aliases for entityList/entityEdit — create stays flat-payload
|
|
51
53
|
// (payloadMode=values); update must accept the {id,version,changes} envelope
|
|
52
54
|
// entityEdit sends (legacy update-tag stays for TagManager). delete stays
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
|
|
2
|
+
import { tagAssignmentExecutor } from "../executor.js";
|
|
3
|
+
export declare function createAssignTagHandler(access?: AccessRule, assignmentExecutor?: typeof tagAssignmentExecutor): WriteHandlerDef;
|
|
3
4
|
export declare const assignTagHandler: WriteHandlerDef;
|
|
@@ -25,7 +25,7 @@ import { assignTagPayloadSchema } from "../schemas.js";
|
|
|
25
25
|
// the aggregate-id is tenant-scoped, so this is not a cross-tenant hole, but
|
|
26
26
|
// without the check any dispatch-eligible tenant user could tag an object they
|
|
27
27
|
// are not allowed to read. See shared/parent-visibility.ts.
|
|
28
|
-
export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS) {
|
|
28
|
+
export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS, assignmentExecutor = tagAssignmentExecutor) {
|
|
29
29
|
return {
|
|
30
30
|
name: "assign-tag",
|
|
31
31
|
schema: assignTagPayloadSchema,
|
|
@@ -37,11 +37,11 @@ export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS) {
|
|
|
37
37
|
if (denied)
|
|
38
38
|
return denied;
|
|
39
39
|
const id = tagAssignmentAggregateId(event.user.tenantId, payload.tagId, payload.entityType, payload.entityId);
|
|
40
|
-
const existing = await
|
|
40
|
+
const existing = await assignmentExecutor.detail({ id }, event.user, ctx.db);
|
|
41
41
|
if (existing) {
|
|
42
42
|
return { isSuccess: true, data: { id } };
|
|
43
43
|
}
|
|
44
|
-
const restored = await
|
|
44
|
+
const restored = await assignmentExecutor.restore({ id }, event.user, ctx.db);
|
|
45
45
|
if (restored.isSuccess)
|
|
46
46
|
return { isSuccess: true, data: { id } };
|
|
47
47
|
// A concurrent first-time assign can also land here: the other caller's
|
|
@@ -57,7 +57,7 @@ export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS) {
|
|
|
57
57
|
const tag = await tagExecutor.detail({ id: payload.tagId }, event.user, ctx.db);
|
|
58
58
|
if (!tag)
|
|
59
59
|
return writeFailure(new NotFoundError("tag", payload.tagId));
|
|
60
|
-
const created = await
|
|
60
|
+
const created = await assignmentExecutor.create({
|
|
61
61
|
id,
|
|
62
62
|
tagId: payload.tagId,
|
|
63
63
|
entityType: payload.entityType,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
|
|
2
|
+
import { tagAssignmentExecutor } from "../executor.js";
|
|
3
|
+
export declare function createRemoveTagHandler(access?: AccessRule, assignmentExecutor?: typeof tagAssignmentExecutor): WriteHandlerDef;
|
|
3
4
|
export declare const removeTagHandler: WriteHandlerDef;
|
|
@@ -10,7 +10,7 @@ import { removeTagPayloadSchema } from "../schemas.js";
|
|
|
10
10
|
// Host reference: entityType/entityId are client input, so the caller must be
|
|
11
11
|
// able to see the host row before detaching anything from it — see
|
|
12
12
|
// shared/parent-visibility.ts.
|
|
13
|
-
export function createRemoveTagHandler(access = DEFAULT_TAG_ACCESS) {
|
|
13
|
+
export function createRemoveTagHandler(access = DEFAULT_TAG_ACCESS, assignmentExecutor = tagAssignmentExecutor) {
|
|
14
14
|
return {
|
|
15
15
|
name: "remove-tag",
|
|
16
16
|
schema: removeTagPayloadSchema,
|
|
@@ -22,11 +22,11 @@ export function createRemoveTagHandler(access = DEFAULT_TAG_ACCESS) {
|
|
|
22
22
|
if (denied)
|
|
23
23
|
return denied;
|
|
24
24
|
const id = tagAssignmentAggregateId(event.user.tenantId, payload.tagId, payload.entityType, payload.entityId);
|
|
25
|
-
const existing = await
|
|
25
|
+
const existing = await assignmentExecutor.detail({ id }, event.user, ctx.db);
|
|
26
26
|
if (!existing) {
|
|
27
27
|
return { isSuccess: true, data: { id } };
|
|
28
28
|
}
|
|
29
|
-
return
|
|
29
|
+
return assignmentExecutor.delete({ id }, event.user, ctx.db);
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
32
|
}
|
|
@@ -28,6 +28,7 @@ export function createClaimTenantHandoverHandler(opts = {}) {
|
|
|
28
28
|
schema: z.object({ token: z.string().min(1), entityType: z.string().min(1) }),
|
|
29
29
|
access: { roles: ["Member", "User", "TenantAdmin", "SystemAdmin"] },
|
|
30
30
|
escapeHatch: {
|
|
31
|
+
grants: ["unsafeRaw"],
|
|
31
32
|
reason: "the ownership change moves rows OUT of a source tenant this caller has no membership " +
|
|
32
33
|
"in — the framework's declared cross-tenant operation for try-before-signup, not a " +
|
|
33
34
|
"per-consumer acknowledgeCrossTenant workaround",
|
|
@@ -15,6 +15,7 @@ export const cancelDestructionWrite = defineWriteHandler({
|
|
|
15
15
|
access: { roles: ["TenantOwner", "Admin"] },
|
|
16
16
|
description: "Returns a tenant whose destruction was requested back to active and clears the grace period; use it to undo a close-account request while the grace period is still running.",
|
|
17
17
|
escapeHatch: {
|
|
18
|
+
grants: ["unsafeRaw"],
|
|
18
19
|
reason: CANCEL_DESTRUCTION_TENANT_ROW_REASON,
|
|
19
20
|
},
|
|
20
21
|
handler: async (event, ctx) => {
|
|
@@ -19,6 +19,7 @@ export const requestDestructionWrite = defineWriteHandler({
|
|
|
19
19
|
description: "Puts the caller's own tenant into destroyRequested, starts the compliance-profile grace period after which its data is erased, and revokes every session in the tenant; use it when an account owner asks to close their account.",
|
|
20
20
|
agent: { risk: "high" },
|
|
21
21
|
escapeHatch: {
|
|
22
|
+
grants: ["unsafeRaw"],
|
|
22
23
|
reason: REQUEST_DESTRUCTION_TENANT_ROW_REASON,
|
|
23
24
|
},
|
|
24
25
|
handler: async (event, ctx) => {
|
|
@@ -15,6 +15,7 @@ export const getTenantTierQuery = defineQueryHandler({
|
|
|
15
15
|
schema: z.object({ tenantId: z.string().min(1) }),
|
|
16
16
|
access: { roles: ["SystemAdmin"] },
|
|
17
17
|
escapeHatch: {
|
|
18
|
+
grants: ["unsafeRaw"],
|
|
18
19
|
reason: GET_TENANT_TIER_REASON,
|
|
19
20
|
},
|
|
20
21
|
handler: async (query, ctx) => {
|
|
@@ -6,6 +6,7 @@ import { attachTenantLabels, dbForList } from "./list.query.js";
|
|
|
6
6
|
// bound to their own tenant's memberships (served by the tenant feature).
|
|
7
7
|
const baseDetail = defineEntityDetailHandler("user", userEntity, {
|
|
8
8
|
access: { roles: access.systemAdmin },
|
|
9
|
+
agent: { risk: "high" },
|
|
9
10
|
description: "Loads one identity record by id for a SystemAdmin, enriched with the same derived `tenants` membership label as the user list; tenant-level admins do not reach it.",
|
|
10
11
|
});
|
|
11
12
|
// Same tenants enrichment as user:list — derived field placeholder is "".
|
|
@@ -6,6 +6,7 @@ import { tenantMembershipsTable, tenantTable } from "../../tenant/index.js";
|
|
|
6
6
|
import { userEntity } from "../schema/user.js";
|
|
7
7
|
const baseList = defineEntityListHandler("user", userEntity, {
|
|
8
8
|
access: { roles: access.systemAdmin },
|
|
9
|
+
agent: { risk: "high" },
|
|
9
10
|
description: "Lists identity records across every tenant for a SystemAdmin roster, each row carrying a derived `tenants` label naming the tenants the user belongs to and their roles there.",
|
|
10
11
|
});
|
|
11
12
|
async function loadMemberships(db, userIds) {
|
|
@@ -27,6 +27,7 @@ export const cancelDeletionWrite = defineWriteHandler({
|
|
|
27
27
|
},
|
|
28
28
|
description: "Withdraws the calling user's own pending account-deletion request and puts the account back to active, accepted only while the grace period is still running.",
|
|
29
29
|
escapeHatch: {
|
|
30
|
+
grants: ["unsafeRaw", "globalWrites"],
|
|
30
31
|
reason: APPEND_LIFECYCLE_EVENT_REASON,
|
|
31
32
|
},
|
|
32
33
|
handler: async (event, ctx) => {
|
|
@@ -5,6 +5,7 @@ import { downloadAttemptEntity } from "../schema/download-attempt.js";
|
|
|
5
5
|
// non-convention QN; entityList needs the convention `download-attempt:list`.
|
|
6
6
|
export const downloadAttemptListQuery = defineEntityListHandler("download-attempt", downloadAttemptEntity, {
|
|
7
7
|
access: { roles: access.systemAdmin },
|
|
8
|
+
agent: { risk: "high" },
|
|
8
9
|
escapeHatch: {
|
|
9
10
|
reason: "SystemAdmin DPO triage of invalid export-download attempts needs every tenant's rows to spot brute-force patterns across tenants",
|
|
10
11
|
},
|
|
@@ -47,6 +47,7 @@ export const downloadByJobQuery = defineQueryHandler({
|
|
|
47
47
|
}, // openToAll = auth-required, kein anonymous
|
|
48
48
|
description: "Returns a short-lived signed download URL for the calling user's own finished data-export job named by job id, backing the download button in the privacy center once export-status reports the job done.",
|
|
49
49
|
escapeHatch: {
|
|
50
|
+
grants: ["unsafeRaw"],
|
|
50
51
|
reason: DOWNLOAD_BY_JOB_REASON,
|
|
51
52
|
},
|
|
52
53
|
handler: async (query, ctx) => {
|
|
@@ -58,6 +58,7 @@ export const downloadByTokenQuery = defineQueryHandler({
|
|
|
58
58
|
// Memory `feedback_security_default_on`.
|
|
59
59
|
rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
|
|
60
60
|
escapeHatch: {
|
|
61
|
+
grants: ["unsafeRaw"],
|
|
61
62
|
reason: DOWNLOAD_BY_TOKEN_REASON,
|
|
62
63
|
},
|
|
63
64
|
handler: async (query, ctx) => {
|
|
@@ -26,6 +26,7 @@ export const exportStatusQuery = defineQueryHandler({
|
|
|
26
26
|
},
|
|
27
27
|
description: "Returns the calling user's own most recent data-export job with its status, expiry and error, or hasJob false, for polling after a request-export while the job is still running.",
|
|
28
28
|
escapeHatch: {
|
|
29
|
+
grants: ["unsafeRaw"],
|
|
29
30
|
reason: EXPORT_STATUS_REASON,
|
|
30
31
|
},
|
|
31
32
|
handler: async (query, ctx) => {
|
|
@@ -33,6 +33,7 @@ export const liftRestrictionWrite = defineWriteHandler({
|
|
|
33
33
|
access: { roles: access.admin },
|
|
34
34
|
description: "Lifts a GDPR Art. 18 processing restriction on the named user and returns the account to active; operator-only, because a restricted user's own session is rejected and cannot reach this endpoint.",
|
|
35
35
|
escapeHatch: {
|
|
36
|
+
grants: ["unsafeRaw", "globalWrites"],
|
|
36
37
|
reason: LIFT_RESTRICTION_ESCAPE_HATCH_REASON,
|
|
37
38
|
},
|
|
38
39
|
handler: async (event, ctx) => {
|
|
@@ -33,6 +33,7 @@ export const myAuditLogQuery = defineQueryHandler({
|
|
|
33
33
|
},
|
|
34
34
|
description: "Returns the calling user's own event-store entries across all their tenant memberships, paged and filterable by aggregate type, event type and time range, for the GDPR Art. 15 self-disclosure; it can never read another user's history.",
|
|
35
35
|
escapeHatch: {
|
|
36
|
+
grants: ["unsafeRaw"],
|
|
36
37
|
reason: MY_AUDIT_LOG_REASON,
|
|
37
38
|
},
|
|
38
39
|
handler: async (query, ctx) => {
|
|
@@ -36,6 +36,7 @@ export function createRequestDeletionByEmailHandler(opts = {}) {
|
|
|
36
36
|
// Defense-in-depth gegen Email-Probing auf dem anonymen Endpoint.
|
|
37
37
|
rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
|
|
38
38
|
escapeHatch: {
|
|
39
|
+
grants: ["unsafeRaw", "globalWrites"],
|
|
39
40
|
reason: APPEND_LIFECYCLE_EVENT_REASON,
|
|
40
41
|
},
|
|
41
42
|
handler: async (event, ctx) => {
|
|
@@ -51,6 +51,7 @@ export const requestExportWrite = defineWriteHandler({
|
|
|
51
51
|
},
|
|
52
52
|
description: "Queues a GDPR Art. 15 and 20 data export for the calling user and returns its job id, handing back the running job with isExisting true instead of a second one when an export is already pending.",
|
|
53
53
|
escapeHatch: {
|
|
54
|
+
grants: ["unsafeRaw"],
|
|
54
55
|
reason: REQUEST_EXPORT_REASON,
|
|
55
56
|
},
|
|
56
57
|
handler: async (event, ctx) => {
|
|
@@ -26,6 +26,7 @@ export function createRunForgetCleanupHandler(opts = {}) {
|
|
|
26
26
|
// records without softDelete — an agent must never be able to trigger it.
|
|
27
27
|
agent: { expose: false, risk: "high" },
|
|
28
28
|
escapeHatch: {
|
|
29
|
+
grants: ["unsafeRaw"],
|
|
29
30
|
reason: RUN_FORGET_CLEANUP_REASON,
|
|
30
31
|
},
|
|
31
32
|
handler: async (_event, ctx) => {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Forget-Cleanup-Runner (S2.U5b) — pure-Function Pipeline.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// After the grace period (S2.U5a sets gracePeriodEnd) this runner iterates
|
|
4
|
+
// over all users in DeletionRequested state and triggers the EXT_USER_DATA
|
|
5
|
+
// delete hooks in every tenant the user is or was a member of (membership
|
|
6
|
+
// history from kumiko_events).
|
|
6
7
|
//
|
|
7
|
-
// **Cross-
|
|
8
|
-
//
|
|
8
|
+
// **Cross-tenant iteration:** a forget request in tenant A must remove the
|
|
9
|
+
// user's data in ALL of their tenants, including tenants they left before
|
|
10
|
+
// the request — see
|
|
9
11
|
// docs/plans/architecture/user-data-rights.md "Cross-Tenant-Semantik".
|
|
10
12
|
//
|
|
11
13
|
// **Strategy-Dispatch:** Pro Entity entscheidet die data-retention-
|
|
@@ -177,15 +179,16 @@ async function processUser(args) {
|
|
|
177
179
|
// memberships in der Tx loeschen. Orphan-User (0 memberships) liefert
|
|
178
180
|
// [] in Email-args; App-Author-Template kann das case-handlen.
|
|
179
181
|
const tenantIdsBeforeDelete = memberships.map((m) => m.tenantId);
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
182
|
+
// Departed tenants: removeMember deletes only the projection row, so data
|
|
183
|
+
// the user left behind there (e.g. note mentions) must still be reached.
|
|
184
|
+
const historicalTenantIds = await readHistoricalMembershipTenantIds(db, userId);
|
|
185
|
+
const hookTenantIds = [...new Set([...tenantIdsBeforeDelete, ...historicalTenantIds])];
|
|
186
|
+
// Edge case "0 tenants" (neither membership nor history): the hook loop still
|
|
187
|
+
// runs with a pseudo tenant. The user hook is tenant-agnostic and MUST run so
|
|
188
|
+
// email/displayName/passwordHash get anonymized; tenant-scoped hooks find
|
|
189
|
+
// nothing there and no-op. Without this path status=Deleted would be set
|
|
190
|
+
// while the original PII stays — compliant-looking, but not compliant.
|
|
191
|
+
const tenantList = hookTenantIds.length > 0 ? hookTenantIds : [SYSTEM_TENANT_ID_FOR_ORPHANS];
|
|
189
192
|
// Per-User-Sub-Tx: hooks + status-flip atomar. Bei Hook-Throw rollt
|
|
190
193
|
// nur dieser User zurueck, andere User bleiben commit-fest. Die Sub-Tx
|
|
191
194
|
// nestet korrekt: eine Top-Level-Connection oeffnet sie via `.begin`
|
|
@@ -342,7 +345,7 @@ async function resolveEffectiveTenantModel(db, tenantId, appTenantModel) {
|
|
|
342
345
|
// rows.
|
|
343
346
|
//
|
|
344
347
|
// Filter by payload->>'tenantId' (not the event column tenant_id): cross-
|
|
345
|
-
// tenant SystemAdmin adds land under the actor's tenant_id
|
|
348
|
+
// tenant SystemAdmin adds land under the actor's tenant_id.
|
|
346
349
|
//
|
|
347
350
|
// Counting created-ROWS against distinct userIds latched a tenant to
|
|
348
351
|
// multi-user as soon as its sole member was removed and re-added (#2608) —
|
|
@@ -369,6 +372,18 @@ async function everHadMultipleMembers(db, tenantId) {
|
|
|
369
372
|
// could name a second member.
|
|
370
373
|
return row.event_count > row.readable_users;
|
|
371
374
|
}
|
|
375
|
+
// Filter by payload (not the event column tenant_id) for the same reason as
|
|
376
|
+
// everHadMultipleMembers: cross-tenant SystemAdmin adds are billed to the
|
|
377
|
+
// actor's tenant.
|
|
378
|
+
async function readHistoricalMembershipTenantIds(db, userId) {
|
|
379
|
+
const rows = await executeRawQuery(db, `SELECT DISTINCT payload->>'tenantId' AS tenant_id
|
|
380
|
+
FROM kumiko_events
|
|
381
|
+
WHERE aggregate_type = 'tenant-membership'
|
|
382
|
+
AND type = $1
|
|
383
|
+
AND payload->>'userId' = $2
|
|
384
|
+
AND NULLIF(payload->>'tenantId', '') IS NOT NULL`, [entityEventName("tenant-membership", "created"), userId]);
|
|
385
|
+
return rows.map((row) => row.tenant_id); // @cast-boundary db-row
|
|
386
|
+
}
|
|
372
387
|
// Mapping retention.strategy → user-data-rights.UserDataDeleteStrategy.
|
|
373
388
|
// - "anonymize" / "blockDelete" → "anonymize" (Aufbewahrungs-Pflicht
|
|
374
389
|
// blockDelete: Daten muessen physisch bleiben, nur Personen-Bezug raus)
|