@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { buildEntityTable, decodeCursor, encodeCursor } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import { definePagedQueryHandler, MAX_LIST_LIMIT } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { definePagedQueryHandler, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { InternalError, ValidationError } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
5
|
import * as z from "zod";
|
|
6
6
|
import { subscriptionsProjectionTable } from "../../billing-foundation/index.js";
|
|
@@ -21,11 +21,16 @@ function billingLabel(subscription, tierSource) {
|
|
|
21
21
|
}
|
|
22
22
|
const FILTER_OP = z.enum(["eq", "ne", "lt", "gt", "in"]);
|
|
23
23
|
const SORTABLE_FIELDS = ["name", "tier", "billing"];
|
|
24
|
-
function
|
|
25
|
-
return JSON.stringify([
|
|
24
|
+
function tenantCapKey(tenantId, capId) {
|
|
25
|
+
return JSON.stringify([tenantId, capId]);
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// One config accessor per tenant: limits may read tenant-scoped config keys, so
|
|
28
|
+
// a limit shared across tenants of the same tier would show the wrong value.
|
|
29
|
+
async function resolveLimitsByTenantAndCap(caps, tenants, configFor) {
|
|
30
|
+
const entries = await Promise.all(tenants.flatMap(({ tenantId, tier }) => {
|
|
31
|
+
const context = { config: configFor?.(tenantId) };
|
|
32
|
+
return caps.map(async (cap) => [tenantCapKey(tenantId, cap.id), await cap.limit(tier, context)]);
|
|
33
|
+
}));
|
|
29
34
|
return new Map(entries);
|
|
30
35
|
}
|
|
31
36
|
function isSortableField(field) {
|
|
@@ -183,7 +188,7 @@ export function createTenantCapsListQuery(caps, listCaps) {
|
|
|
183
188
|
const perTenantEntries = await Promise.all(pageTenantIds.map(async (tenantId) => [tenantId, await cap.usage(db, tenantId)]));
|
|
184
189
|
usageByCap.set(cap.id, new Map(perTenantEntries));
|
|
185
190
|
}));
|
|
186
|
-
const
|
|
191
|
+
const limitByTenantAndCap = await resolveLimitsByTenantAndCap(listedCaps, page, ctx.configFor);
|
|
187
192
|
const rows = page.map((row) => {
|
|
188
193
|
const capFields = {};
|
|
189
194
|
for (const cap of listedCaps) {
|
|
@@ -191,7 +196,7 @@ export function createTenantCapsListQuery(caps, listCaps) {
|
|
|
191
196
|
// only an explicit `null` value means "not measured".
|
|
192
197
|
const rawUsed = usageByCap.get(cap.id)?.get(row.tenantId);
|
|
193
198
|
const used = rawUsed === undefined ? 0 : rawUsed;
|
|
194
|
-
const limit =
|
|
199
|
+
const limit = limitByTenantAndCap.get(tenantCapKey(row.tenantId, cap.id)) ?? null;
|
|
195
200
|
capFields[capFieldName(cap.id)] =
|
|
196
201
|
used === null
|
|
197
202
|
? { used: null, limit, fraction: 0 }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { DEFAULT_CAP_USAGE_ROLES } from "./access.js";
|
|
2
2
|
export { CAP_CARDS_PANEL_COMPONENT, CAP_OVERVIEW_FEATURE, CAP_USAGE_CELL_COMPONENT, CapOverviewQueries, capFieldName, MY_CAPS_SCREEN_ID, PLATFORM_TENANT_CAPS_SCREEN_ID, TENANT_CAP_LIST_SCREEN_ID, } from "./constants.js";
|
|
3
3
|
export { type CreateCapOverviewOptions, createCapOverviewFeature } from "./feature.js";
|
|
4
|
-
export {
|
|
4
|
+
export { createMyCapsScreen, createTenantCapListScreen, platformTenantCapsScreen, } from "./screens.js";
|
|
5
5
|
export type { CapSpec, CapUsage, CapUsageTone, CapUsageWithMeta } from "./types.js";
|
|
6
6
|
export { computeFraction, computeTone } from "./usage-math.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Public API of the cap-overview bundled-feature.
|
|
2
|
-
export {
|
|
2
|
+
export { DEFAULT_CAP_USAGE_ROLES } from "./access.js";
|
|
3
3
|
export { CAP_CARDS_PANEL_COMPONENT, CAP_OVERVIEW_FEATURE, CAP_USAGE_CELL_COMPONENT, CapOverviewQueries, capFieldName, MY_CAPS_SCREEN_ID, PLATFORM_TENANT_CAPS_SCREEN_ID, TENANT_CAP_LIST_SCREEN_ID, } from "./constants.js";
|
|
4
4
|
export { createCapOverviewFeature } from "./feature.js";
|
|
5
|
-
export {
|
|
5
|
+
export { createMyCapsScreen, createTenantCapListScreen, platformTenantCapsScreen, } from "./screens.js";
|
|
6
6
|
export { computeFraction, computeTone } from "./usage-math.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ScreenDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import type { CapSpec } from "./types.js";
|
|
3
3
|
export declare function createTenantCapListScreen(caps: readonly CapSpec[], listCaps: readonly string[], tiers?: readonly string[]): ScreenDefinition;
|
|
4
|
-
export declare
|
|
4
|
+
export declare function createMyCapsScreen(usageRoles: readonly string[]): ScreenDefinition;
|
|
5
5
|
export declare const platformTenantCapsScreen: ScreenDefinition;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { MY_CAPS_ACCESS_ROLES } from "./access.js";
|
|
2
1
|
import { CAP_CARDS_PANEL_COMPONENT, CAP_USAGE_CELL_COMPONENT, CapOverviewQueries, capFieldName, MY_CAPS_SCREEN_ID, PLATFORM_TENANT_CAPS_SCREEN_ID, TENANT_CAP_LIST_SCREEN_ID, } from "./constants.js";
|
|
3
2
|
// `tiers` has no room for a separate per-tier display label (it's just
|
|
4
3
|
// `readonly string[]`) — tier-engine itself has no enumerated tier list to
|
|
@@ -55,21 +54,23 @@ export function createTenantCapListScreen(caps, listCaps, tiers) {
|
|
|
55
54
|
dormant: true,
|
|
56
55
|
};
|
|
57
56
|
}
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
export function createMyCapsScreen(usageRoles) {
|
|
58
|
+
return {
|
|
59
|
+
id: MY_CAPS_SCREEN_ID,
|
|
60
|
+
type: "dashboard",
|
|
61
|
+
panels: [
|
|
62
|
+
{
|
|
63
|
+
kind: "custom",
|
|
64
|
+
id: "cap-cards",
|
|
65
|
+
component: { react: { __component: CAP_CARDS_PANEL_COMPONENT } },
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
access: { roles: usageRoles },
|
|
69
|
+
// No nav wiring yet (see feature.ts header) — no nav area to resolve in
|
|
70
|
+
// isolation.
|
|
71
|
+
dormant: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
73
74
|
export const platformTenantCapsScreen = {
|
|
74
75
|
id: PLATFORM_TENANT_CAPS_SCREEN_ID,
|
|
75
76
|
type: "dashboard",
|
|
@@ -2,7 +2,7 @@ import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
|
2
2
|
import { configuredPiiSubjectKms, encryptPiiValueForSubject, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
3
3
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
4
4
|
import { ConfigScopes, defineWriteHandler, isEncryptedAtRest, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
-
import { InternalError, UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
|
+
import { InternalError, isKumikoError, UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
6
6
|
import * as z from "zod";
|
|
7
7
|
import { requireConfigEncryption, requireSystemDb } from "../feature.js";
|
|
8
8
|
import { MASKED } from "../read-redaction.js";
|
|
@@ -14,7 +14,7 @@ const executor = createEventStoreExecutor(configValuesTable, configValueEntity,
|
|
|
14
14
|
});
|
|
15
15
|
export const setWrite = defineWriteHandler({
|
|
16
16
|
name: "set",
|
|
17
|
-
description: "Stores a config value for one key at the requested scope (user, tenant or system) after type, bounds and
|
|
17
|
+
description: "Stores a config value for one key at the requested scope (user, tenant or system) after type, bounds, pattern and the key's own validator, encrypting it when the key declares that; use it to change a setting.",
|
|
18
18
|
schema: z.object({
|
|
19
19
|
key: z.string(),
|
|
20
20
|
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
@@ -67,6 +67,22 @@ export const setWrite = defineWriteHandler({
|
|
|
67
67
|
const patternError = validatePattern(event.payload.value, keyDef);
|
|
68
68
|
if (patternError)
|
|
69
69
|
return writeFailure(patternError);
|
|
70
|
+
if (keyDef.validate) {
|
|
71
|
+
try {
|
|
72
|
+
await keyDef.validate(event.payload.value, {
|
|
73
|
+
key: event.payload.key,
|
|
74
|
+
scope,
|
|
75
|
+
tenantId,
|
|
76
|
+
userId,
|
|
77
|
+
db,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (isKumikoError(error))
|
|
82
|
+
return writeFailure(error);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
70
86
|
// The batch idempotency cache stores this result and only masks entity
|
|
71
87
|
// writeOnly fields, so an at-rest-encrypted value must not be echoed.
|
|
72
88
|
const echoedValue = isEncryptedAtRest(keyDef) ? MASKED : event.payload.value;
|
|
@@ -8,8 +8,9 @@ export function createCryptoShreddingFeature() {
|
|
|
8
8
|
displayLabel: "Crypto-Shredding",
|
|
9
9
|
category: "compliance",
|
|
10
10
|
});
|
|
11
|
-
// `reason` is operator free text and may name the subject
|
|
12
|
-
// the subject key
|
|
11
|
+
// `reason` is operator free text and may name the subject, yet stays plaintext: the forget
|
|
12
|
+
// erases the subject key before the audit append, so a forget of the actor's own tenant
|
|
13
|
+
// would fail to seal the reason under that tenant's key.
|
|
13
14
|
r.defineEvent("subject-forgotten", subjectForgottenSchema, { piiFields: "none" });
|
|
14
15
|
r.defineEvent("forget-denied", subjectForgetDeniedSchema, { piiFields: "none" });
|
|
15
16
|
r.writeHandler(forgetSubjectWrite);
|
|
@@ -21,7 +21,7 @@ export declare const subjectForgottenSchema: z.ZodObject<{
|
|
|
21
21
|
forgottenBy: z.ZodString;
|
|
22
22
|
}, z.core.$strip>;
|
|
23
23
|
export declare const subjectForgetDeniedSchema: z.ZodObject<{
|
|
24
|
-
subjectKeyDigest: z.ZodString
|
|
24
|
+
subjectKeyDigest: z.ZodOptional<z.ZodString>;
|
|
25
25
|
subjectKind: z.ZodEnum<{
|
|
26
26
|
record: "record";
|
|
27
27
|
tenant: "tenant";
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
3
2
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
4
|
-
import { configuredPiiSubjectKms, subjectIdSchema, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
5
|
-
import { nullBlindIndexesForSubject, recordRowExistsInTenant, recordRowOwningTenantId, subjectRowExistsInTenant, } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { computeBlindIndex, configuredBlindIndexKey, configuredPiiSubjectKms, subjectIdSchema, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
4
|
+
import { nullBlindIndexesForSubject, recordEventsOwnedExclusivelyByTenant, recordRowExistsInTenant, recordRowOwningTenantId, subjectRowExistsInTenant, } from "@cosmicdrift/kumiko-framework/db";
|
|
6
5
|
import { defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
6
|
import { AccessDeniedError, InternalError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
7
|
import { append } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -26,7 +25,7 @@ export const subjectForgottenSchema = z.object({
|
|
|
26
25
|
forgottenBy: z.string().min(1),
|
|
27
26
|
});
|
|
28
27
|
export const subjectForgetDeniedSchema = z.object({
|
|
29
|
-
subjectKeyDigest: z.string().min(1),
|
|
28
|
+
subjectKeyDigest: z.string().min(1).optional(),
|
|
30
29
|
subjectKind: z.enum(["user", "tenant", "record"]),
|
|
31
30
|
reason: z.string().min(10),
|
|
32
31
|
forgottenBy: z.string().min(1),
|
|
@@ -78,13 +77,20 @@ async function resolveTenantScopeDenial(db, features, user, raw) {
|
|
|
78
77
|
if (!features.has("tenant"))
|
|
79
78
|
return undefined;
|
|
80
79
|
const entity = findRegisteredEntity(features, raw.entity);
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
const rowInTenant = entity !== undefined &&
|
|
81
|
+
(await recordRowExistsInTenant(db, features, raw.entity, raw.id, user.tenantId));
|
|
82
|
+
if (rowInTenant)
|
|
83
|
+
return undefined;
|
|
84
|
+
// A deleted row or a custom aggregate without an entity has no projection
|
|
85
|
+
// row to check; its event stream still proves which tenant owns it.
|
|
86
|
+
if (await recordEventsOwnedExclusivelyByTenant(db, raw.entity, raw.id, user.tenantId)) {
|
|
87
|
+
return undefined;
|
|
83
88
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
return writeFailure(new AccessDeniedError({
|
|
90
|
+
details: {
|
|
91
|
+
reason: entity ? TARGET_RECORD_NOT_ADMIN_TENANT : RECORD_ENTITY_NOT_REGISTERED,
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
88
94
|
}
|
|
89
95
|
// Without the tenant feature there's no membership table to check against —
|
|
90
96
|
// no scoping concept exists to enforce (single/no-tenant apps only;
|
|
@@ -160,15 +166,19 @@ async function appendDenialAuditEvent(ctx, event, subjectKey, subjectKind, denia
|
|
|
160
166
|
message: `[crypto-shredding] event "${SUBJECT_FORGET_DENIED_EVENT_NAME}" is not registered.`,
|
|
161
167
|
}));
|
|
162
168
|
}
|
|
169
|
+
const blindIndexKey = configuredBlindIndexKey();
|
|
163
170
|
const payload = subjectForgetDeniedSchema.parse({
|
|
164
171
|
// The denial lands in the REQUESTING actor's own tenant-scoped stream —
|
|
165
172
|
// it must never materialise the foreign subject's identifiers there. A
|
|
166
173
|
// plaintext subjectKey/aggregateId would survive as a permanent record
|
|
167
174
|
// for the prober and would still be present when the owning tenant
|
|
168
175
|
// later runs its own (legitimate) forget-subject for that subject. The
|
|
169
|
-
// digest
|
|
170
|
-
//
|
|
171
|
-
|
|
176
|
+
// digest is keyed (a bare hash of a guessable id could be recomputed from
|
|
177
|
+
// a candidate id) and lets an operator correlate repeated probes of the
|
|
178
|
+
// same subject; without a blind-index key no digest is stored at all.
|
|
179
|
+
...(blindIndexKey !== undefined && {
|
|
180
|
+
subjectKeyDigest: computeBlindIndex(blindIndexKey, subjectKey),
|
|
181
|
+
}),
|
|
172
182
|
subjectKind,
|
|
173
183
|
reason: event.payload.reason,
|
|
174
184
|
forgottenBy: event.user.id,
|
|
@@ -216,6 +226,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
216
226
|
// not be able to reach it at all.
|
|
217
227
|
agent: { expose: false, risk: "high" },
|
|
218
228
|
escapeHatch: {
|
|
229
|
+
grants: ["unsafeRaw"],
|
|
219
230
|
reason: "denial audit append names the prober's own tenant stream on the outside-transaction db; " +
|
|
220
231
|
"the tenant-scope and retention checks run against the subject's tenant, not necessarily the caller's; " +
|
|
221
232
|
"the blind-index sweep and search purge address the subject across tenants; the user " +
|
|
@@ -136,12 +136,15 @@ export function resolveFieldDefinitionListRoles(opts) {
|
|
|
136
136
|
if (opts.valueWriteRoles === undefined && opts.fieldDefinitionWriteRoles === undefined) {
|
|
137
137
|
return DEFAULT_FIELD_DEFINITION_LIST_ROLES;
|
|
138
138
|
}
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
// The default stays in the union: with valueWriteRoles unset, TenantAdmin still
|
|
140
|
+
// saves values and must be able to load the FormSection.
|
|
141
|
+
return [
|
|
142
|
+
...new Set([
|
|
143
|
+
...(opts.valueWriteRoles ?? []),
|
|
144
|
+
...(opts.fieldDefinitionWriteRoles ?? []),
|
|
145
|
+
...DEFAULT_FIELD_DEFINITION_LIST_ROLES,
|
|
146
|
+
]),
|
|
147
|
+
];
|
|
145
148
|
}
|
|
146
149
|
// Backwards-compat-wrapper. Bestehende Caller (z.B. integration-tests,
|
|
147
150
|
// host-apps) nutzen weiterhin `createCustomFieldsFeature()`. Returnt den
|
|
@@ -26,6 +26,7 @@ export const clearCustomFieldHandler = {
|
|
|
26
26
|
access: { roles: DEFAULT_VALUE_WRITE_ROLES },
|
|
27
27
|
description: "Removes the stored value of one custom field from a single host entity row after re-checking that field's per-field write roles; use it to blank a field the user emptied instead of writing a null value.",
|
|
28
28
|
escapeHatch: {
|
|
29
|
+
grants: ["unsafeRaw"],
|
|
29
30
|
reason: CLEAR_CUSTOM_FIELD_REASON,
|
|
30
31
|
},
|
|
31
32
|
handler: async (event, ctx) => {
|
|
@@ -15,6 +15,7 @@ export function createDefineTenantFieldHandler(opts = {}) {
|
|
|
15
15
|
access: { roles: opts.roles ?? DEFAULT_FIELD_DEFINITION_WRITE_ROLES },
|
|
16
16
|
description: "Creates a custom-field definition owned by the caller's own tenant on the named entity, rejecting the write once the tenant's definition quota is reached; use it when one tenant needs an extra field the other tenants must not see.",
|
|
17
17
|
escapeHatch: {
|
|
18
|
+
grants: ["unsafeRaw"],
|
|
18
19
|
reason: DEFINE_TENANT_FIELD_REASON,
|
|
19
20
|
},
|
|
20
21
|
handler: async (event, ctx) => {
|
|
@@ -49,6 +49,7 @@ export const setCustomFieldHandler = {
|
|
|
49
49
|
access: { roles: DEFAULT_VALUE_WRITE_ROLES },
|
|
50
50
|
description: "Stores one custom-field value on a single host entity row after validating it against the field definition's declared type and per-field write roles; use it to save what a user entered into a custom field.",
|
|
51
51
|
escapeHatch: {
|
|
52
|
+
grants: ["unsafeRaw"],
|
|
52
53
|
reason: SET_CUSTOM_FIELD_REASON,
|
|
53
54
|
},
|
|
54
55
|
handler: async (event, ctx) => {
|
|
@@ -88,16 +88,18 @@ export function createDeliveryService(options) {
|
|
|
88
88
|
sink: escapeHatchAuditSink,
|
|
89
89
|
log,
|
|
90
90
|
});
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
91
|
+
// ctx.db/dbOutsideTransaction are tenant-filtered even for a systemScope handler:
|
|
92
|
+
// the dispatcher closes them off entirely there, so "system" mode would hand this
|
|
93
|
+
// path a cross-tenant read the dispatcher never allows. Cross-tenant reach only
|
|
94
|
+
// exists through the grant-carrying systemDb view below.
|
|
95
95
|
const isSystem = registry.isHandlerSystemScoped(tenantUserIdsQuery);
|
|
96
|
-
const tenantDb = createTenantDb(db, tenantId,
|
|
96
|
+
const tenantDb = createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
|
|
97
|
+
report,
|
|
98
|
+
});
|
|
97
99
|
// The dispatcher grants a systemScope handler's ctx.systemDb.unsafeRaw() ungated —
|
|
98
100
|
// the grant comes from the handler's own r.systemScope() registration, not from this
|
|
99
|
-
// caller.
|
|
100
|
-
//
|
|
101
|
+
// caller. Only the systemDb view's own source TenantDb carries the grant, so it
|
|
102
|
+
// can't leak onto ctx.db.
|
|
101
103
|
const systemDbSourceTenantDb = isSystem
|
|
102
104
|
? createTenantDb(db, tenantId, "system", undefined, undefined, undefined, {
|
|
103
105
|
report,
|
|
@@ -114,8 +114,9 @@ export const documentIngestFoundationFeature = defineFeature(FEATURE_NAME, (r) =
|
|
|
114
114
|
write: access.roles("TenantAdmin", "SystemAdmin"),
|
|
115
115
|
read: access.roles("TenantAdmin", "SystemAdmin", "User"),
|
|
116
116
|
}));
|
|
117
|
-
// "fileName" can carry a real person's name
|
|
118
|
-
//
|
|
117
|
+
// "fileName" can carry a real person's name, but it reaches both events already as the
|
|
118
|
+
// fileRef's own self-subject ciphertext (copied from fileRef.created/restored), which
|
|
119
|
+
// append() passes through; shredding the fileRef's key shreds it here too.
|
|
119
120
|
r.defineEvent(DOCUMENT_INGEST_REQUESTED_EVENT_SHORT, documentIngestRequestedPayloadSchema, {
|
|
120
121
|
piiFields: "none",
|
|
121
122
|
version: 2,
|
|
@@ -29,6 +29,7 @@ export const discardDraftWrite = defineWriteHandler({
|
|
|
29
29
|
description: "Deletes the calling user's draft for one draftKey and, when releaseFiles is set, hard-deletes the file references that draft alone uploaded; use it after a successful submit or when the user abandons the form.",
|
|
30
30
|
agent: { risk: "high" },
|
|
31
31
|
escapeHatch: {
|
|
32
|
+
grants: ["unsafeRaw"],
|
|
32
33
|
reason: DISCARD_DRAFT_FILE_REFS_REASON,
|
|
33
34
|
},
|
|
34
35
|
handler: async (event, ctx) => {
|
|
@@ -27,6 +27,7 @@ export const saveDraftWrite = defineWriteHandler({
|
|
|
27
27
|
access: FORM_DRAFT_ACCESS,
|
|
28
28
|
description: "Upserts the calling user's draft for one draftKey with the given form values and step index, stamping savedAt server-side and refusing a brand-new draft once the per-owner draft cap is reached; use it to persist an in-progress form before the real entity exists.",
|
|
29
29
|
escapeHatch: {
|
|
30
|
+
grants: ["unsafeRaw"],
|
|
30
31
|
reason: SAVE_DRAFT_COUNT_REASON,
|
|
31
32
|
},
|
|
32
33
|
handler: async (event, ctx) => {
|
|
@@ -38,6 +38,7 @@ export const setWrite = defineWriteHandler({
|
|
|
38
38
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
39
39
|
description: "Creates or overwrites one managed page addressed by slug and language, keeping the existing published flag, description and OG image when the payload omits them; use it for content edits and publish toggles, and as SystemAdmin to write another tenant's pages.",
|
|
40
40
|
escapeHatch: {
|
|
41
|
+
grants: ["unsafeRaw"],
|
|
41
42
|
reason: SET_PAGE_TENANT_OVERRIDE_REASON,
|
|
42
43
|
},
|
|
43
44
|
handler: async (event, ctx) => {
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
import { type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
2
|
export declare const noteEntryExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor, noteEntryTable: import("@cosmicdrift/kumiko-framework/db").EntityTable;
|
|
3
|
+
export declare function createNoteEntryExecutor(ownership: EntityDefinition["access"] | undefined): typeof noteEntryExecutor;
|
|
2
4
|
export declare const noteMentionExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor, noteMentionTable: import("@cosmicdrift/kumiko-framework/db").EntityTable;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { createEntityExecutor } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { noteEntryEntity, noteMentionEntity } from "./entity.js";
|
|
2
|
+
import { createNoteEntryEntity, noteEntryEntity, noteMentionEntity } from "./entity.js";
|
|
3
3
|
export const { executor: noteEntryExecutor, table: noteEntryTable } = createEntityExecutor("note-entry", noteEntryEntity);
|
|
4
|
+
// Carries only `ownership.write`: the executor enforces entity.access.write on
|
|
5
|
+
// create, while read ownership is applied by the list handler.
|
|
6
|
+
export function createNoteEntryExecutor(ownership) {
|
|
7
|
+
if (ownership?.write === undefined)
|
|
8
|
+
return noteEntryExecutor;
|
|
9
|
+
return createEntityExecutor("note-entry", createNoteEntryEntity({ write: ownership.write }))
|
|
10
|
+
.executor;
|
|
11
|
+
}
|
|
4
12
|
export const { executor: noteMentionExecutor, table: noteMentionTable } = createEntityExecutor("note-mention", noteMentionEntity);
|
|
@@ -19,9 +19,8 @@ export type NotesHistoryFeatureOptions = {
|
|
|
19
19
|
* it is AND-ed with the host-visibility gate, never a replacement for it.
|
|
20
20
|
*
|
|
21
21
|
* `ownership.write` is separate and does NOT affect list/read. It's
|
|
22
|
-
* consulted
|
|
23
|
-
*
|
|
24
|
-
* boot-guard comment). A `from()` rule there also gates GDPR erasure
|
|
22
|
+
* consulted on add-note (create) and the framework's generic
|
|
23
|
+
* forget/restore paths. A `from()` rule there also gates GDPR erasure
|
|
25
24
|
* (`forget`): if the rule's role map doesn't cover whatever role the
|
|
26
25
|
* erasure/retention pipeline runs as, `forget` denies instead of
|
|
27
26
|
* crypto-shredding — a silent Art.17 failure, not a thrown error. Make
|
|
@@ -16,6 +16,7 @@ import { defineEntityListHandler, defineFeature, } from "@cosmicdrift/kumiko-fra
|
|
|
16
16
|
import { hasWhereRule } from "../shared/index.js";
|
|
17
17
|
import { DEFAULT_NOTES_HISTORY_ACCESS, NOTES_HISTORY_FEATURE_NAME } from "./constants.js";
|
|
18
18
|
import { createNoteEntryEntity, noteMentionEntity } from "./entity.js";
|
|
19
|
+
import { createNoteEntryExecutor } from "./executor.js";
|
|
19
20
|
import { createAddNoteHandler } from "./handlers/add-note.write.js";
|
|
20
21
|
import { NOTES_HISTORY_FEATURE_I18N } from "./i18n.js";
|
|
21
22
|
function registerNotesHistory(r, access, ownership, parents) {
|
|
@@ -33,7 +34,7 @@ function registerNotesHistory(r, access, ownership, parents) {
|
|
|
33
34
|
// just building it inline in the handler) is what makes it visible to the
|
|
34
35
|
// registry-wide GDPR boot guards and to executor.ts's table/projection setup.
|
|
35
36
|
r.entity("note-mention", noteMentionEntity);
|
|
36
|
-
r.writeHandler(createAddNoteHandler(access));
|
|
37
|
+
r.writeHandler(createAddNoteHandler(access, createNoteEntryExecutor(ownership)));
|
|
37
38
|
r.queryHandler(defineEntityListHandler("note-entry", entity, {
|
|
38
39
|
access,
|
|
39
40
|
description: "Lists note-history entries of the caller's tenant with their text, author and timestamp; filter on entityId to read the full note trail of one record.",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
|
|
2
|
+
import { noteEntryExecutor } from "../executor.js";
|
|
3
|
+
export declare function createAddNoteHandler(access?: AccessRule, entryExecutor?: typeof noteEntryExecutor): WriteHandlerDef;
|
|
3
4
|
export declare const addNoteHandler: WriteHandlerDef;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { fetchOne, runInSavepointIfSupported } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
3
|
import { decryptStoredPii, denyUnlessJoinRowParentVisible } from "../../shared/index.js";
|
|
4
|
+
import { tenantMembershipsTable } from "../../tenant/index.js";
|
|
3
5
|
import { userTable } from "../../user/index.js";
|
|
4
6
|
import { DEFAULT_NOTES_HISTORY_ACCESS } from "../constants.js";
|
|
5
7
|
import { noteEntryExecutor, noteMentionExecutor } from "../executor.js";
|
|
@@ -17,13 +19,14 @@ const READ_AUTHOR_DISPLAY_NAME_REASON = "reads the author's displayName from the
|
|
|
17
19
|
// Both the field names and the optional `parents` allowlist come from the
|
|
18
20
|
// entity's own `parentRef` declaration, which the read gate reads too —
|
|
19
21
|
// see shared/parent-visibility.ts.
|
|
20
|
-
export function createAddNoteHandler(access = DEFAULT_NOTES_HISTORY_ACCESS) {
|
|
22
|
+
export function createAddNoteHandler(access = DEFAULT_NOTES_HISTORY_ACCESS, entryExecutor = noteEntryExecutor) {
|
|
21
23
|
return {
|
|
22
24
|
name: "add-note",
|
|
23
25
|
schema: addNotePayloadSchema,
|
|
24
26
|
access,
|
|
25
27
|
description: "Appends a note to one host entity's history, stamping the author from the authenticated caller rather than the payload; use it for every remark and correction alike, because entries can never be edited or removed afterwards.",
|
|
26
28
|
escapeHatch: {
|
|
29
|
+
grants: ["unsafeRaw"],
|
|
27
30
|
reason: READ_AUTHOR_DISPLAY_NAME_REASON,
|
|
28
31
|
},
|
|
29
32
|
handler: async (event, ctx) => {
|
|
@@ -31,6 +34,20 @@ export function createAddNoteHandler(access = DEFAULT_NOTES_HISTORY_ACCESS) {
|
|
|
31
34
|
const denied = await denyUnlessJoinRowParentVisible(ctx.registry, "note-entry", payload, event.user, ctx.db);
|
|
32
35
|
if (denied)
|
|
33
36
|
return denied;
|
|
37
|
+
// A mention of a non-member would leave a note body about that person out of reach of
|
|
38
|
+
// their forget run, which only visits the tenants they are a member of.
|
|
39
|
+
const mentionedUserIds = [...new Set(payload.mentions ?? [])];
|
|
40
|
+
if (mentionedUserIds.length > 0) {
|
|
41
|
+
const memberRows = await ctx.db.selectMany(tenantMembershipsTable, {
|
|
42
|
+
userId: { in: mentionedUserIds },
|
|
43
|
+
});
|
|
44
|
+
const memberIds = new Set(memberRows.map((row) => row.userId));
|
|
45
|
+
if (mentionedUserIds.some((userId) => !memberIds.has(userId))) {
|
|
46
|
+
return writeFailure(new ValidationError({
|
|
47
|
+
fields: [{ path: "mentions", code: "custom", i18nKey: "errors.validation.custom" }],
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
34
51
|
let authorName = null;
|
|
35
52
|
try {
|
|
36
53
|
// read_users is tenant-agnostic → ctx.db.unsafeRaw, not the tenant-scoped ctx.db.
|
|
@@ -54,13 +71,13 @@ export function createAddNoteHandler(access = DEFAULT_NOTES_HISTORY_ACCESS) {
|
|
|
54
71
|
authorName = null;
|
|
55
72
|
}
|
|
56
73
|
const { mentions, ...notePayload } = payload;
|
|
57
|
-
const created = await
|
|
74
|
+
const created = await entryExecutor.create({ ...notePayload, authorId: event.user.id, authorName }, event.user, ctx.db);
|
|
58
75
|
if (!created.isSuccess)
|
|
59
76
|
return created;
|
|
60
77
|
// Same tx as the note create above (ctx.db) — a mention-row failure
|
|
61
78
|
// rolls the note back with it, same as the framework's own nested-write
|
|
62
79
|
// parent+child pattern (dispatch-write.ts).
|
|
63
|
-
for (const subjectId of
|
|
80
|
+
for (const subjectId of mentionedUserIds) {
|
|
64
81
|
const mentionResult = await noteMentionExecutor.create({ noteId: created.data.id, subjectId }, event.user, ctx.db);
|
|
65
82
|
if (!mentionResult.isSuccess)
|
|
66
83
|
return mentionResult;
|
|
@@ -93,12 +93,30 @@ export const noteEntryDeleteHook = async (ctx) => {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
// Erasure reaches every note that structurally mentions the user (see
|
|
97
|
+
// noteEntryDeleteHook), so the Art. 15/20 export must list those notes too.
|
|
98
|
+
export const noteMentionExportHook = async (ctx) => {
|
|
99
|
+
const mentions = await ctx.db.selectMany(noteMentionTable, {
|
|
100
|
+
subjectId: ctx.userId,
|
|
101
|
+
});
|
|
102
|
+
if (mentions.length === 0)
|
|
103
|
+
return null;
|
|
104
|
+
const noteIds = [...new Set(mentions.map((m) => m.noteId))];
|
|
105
|
+
const notes = await ctx.db.selectMany(noteEntryTable, { id: { in: noteIds } });
|
|
106
|
+
if (notes.length === 0)
|
|
107
|
+
return null;
|
|
108
|
+
return {
|
|
109
|
+
entity: "note-mention",
|
|
110
|
+
rows: notes.map((note) => ({
|
|
111
|
+
noteId: note["id"],
|
|
112
|
+
entityType: note["entityType"],
|
|
113
|
+
entityId: note["entityId"],
|
|
114
|
+
body: note["body"],
|
|
115
|
+
insertedAt: note["insertedAt"],
|
|
116
|
+
})),
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
// note-mention rows are a plain (noteId, subjectId) pointer: forgetting the mentioned
|
|
120
|
+
// user's data is already handled by noteEntryDeleteHook shredding the mentioned NOTE's
|
|
121
|
+
// row-subject key, so this delete hook has nothing of its own to erase.
|
|
104
122
|
export const noteMentionDeleteHook = async () => { };
|
|
@@ -37,6 +37,7 @@ export const revokeAllForUserWrite = defineWriteHandler({
|
|
|
37
37
|
description: "Irreversibly signs a named user out of all their live sessions, across every tenant unless a tenantId narrows it, optionally sparing one session (exceptSessionId); use it for operator actions such as freezing or banning an account, or a role change that must not end the editor's own session.",
|
|
38
38
|
agent: { risk: "high" },
|
|
39
39
|
escapeHatch: {
|
|
40
|
+
grants: ["unsafeRaw"],
|
|
40
41
|
reason: REVOKE_ALL_SESSIONS_REASON,
|
|
41
42
|
},
|
|
42
43
|
handler: async (event, ctx) => {
|
|
@@ -49,6 +49,7 @@ export function createMollieCheckoutSession(client, priceToConfig, appWebhookUrl
|
|
|
49
49
|
metadata: {
|
|
50
50
|
tenantId: options.tenantId,
|
|
51
51
|
priceId: options.priceId,
|
|
52
|
+
...(options.consentId && { consentId: options.consentId }),
|
|
52
53
|
},
|
|
53
54
|
})); // @cast-boundary engine-bridge
|
|
54
55
|
const checkoutHref = payment.getCheckoutUrl();
|