@cosmicdrift/kumiko-bundled-features 0.336.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/agent-manifest.js +34 -7
- package/dist/agent-tools/tool-catalog.js +58 -19
- package/dist/audit/handlers/list.query.js +7 -3
- 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-email-password/seeding.js +6 -0
- package/dist/auth-email-password/web/invite-accept-screen.js +4 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -0
- package/dist/auth-mfa/handlers/enable-start.write.js +13 -2
- package/dist/auth-mfa/hmac-token-codec.js +8 -9
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +2 -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 +13 -13
- 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/config/write-helpers.js +2 -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/custom-fields/lib/field-access.d.ts +3 -2
- package/dist/custom-fields/lib/field-access.js +1 -0
- package/dist/delivery/delivery-service.js +9 -7
- package/dist/delivery/unsubscribe.js +1 -0
- package/dist/document-ingest-foundation/feature.js +3 -2
- package/dist/files-tenant-data/hooks.js +11 -3
- package/dist/folders/web/folder-section.js +8 -1
- 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/jobs/constants.d.ts +1 -0
- package/dist/jobs/constants.js +1 -0
- package/dist/jobs/handlers/trigger.write.js +7 -2
- package/dist/jobs/i18n.js +3 -0
- package/dist/managed-pages/handlers/set.write.js +1 -0
- package/dist/metrics/compute.js +16 -5
- 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/web/notes-section.js +6 -1
- package/dist/notes-history-user-data/hooks.js +26 -8
- package/dist/rate-limiting/handlers/bucket-access.d.ts +2 -5
- package/dist/secrets/feature.js +2 -0
- package/dist/secrets/handlers/delete.write.js +4 -0
- package/dist/secrets/handlers/set.write.js +5 -2
- package/dist/secrets/i18n.d.ts +10 -0
- package/dist/secrets/i18n.js +10 -0
- package/dist/secrets/write-gate.d.ts +10 -0
- package/dist/secrets/write-gate.js +36 -0
- package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -0
- package/dist/shared/index.d.ts +0 -1
- package/dist/shared/index.js +0 -1
- package/dist/shared/session-locale-field.js +1 -2
- package/dist/shared/session-timezone-field.js +3 -2
- package/dist/step-dispatcher/feature.js +4 -3
- package/dist/step-dispatcher/mail-runner.d.ts +1 -0
- package/dist/step-dispatcher/mail-runner.js +5 -0
- package/dist/step-dispatcher/webhook-runner.d.ts +7 -0
- package/dist/step-dispatcher/webhook-runner.js +9 -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 +85 -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-handover/move-entity-graph.js +11 -3
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +1 -0
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +1 -0
- package/dist/tenant-lifecycle/lifecycle-gate.d.ts +4 -0
- package/dist/tenant-lifecycle/lifecycle-gate.js +26 -4
- 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/deletion-grace-period.js +3 -1
- 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/dist/workflow-runner/db/queries/due-runs.d.ts +1 -0
- package/dist/workflow-runner/db/queries/due-runs.js +4 -1
- package/package.json +9 -9
- package/src/agent-tools/changes.json +20 -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/secrets/changes.json +7 -0
- package/src/subscription-mollie/changes.json +13 -0
- package/src/subscription-stripe/changes.json +19 -0
- package/src/tags/changes.json +7 -0
- package/src/tenant-lifecycle/changes.json +8 -1
- package/src/user-data-rights/changes.json +6 -0
- package/src/workflow-runner/changes.json +6 -0
- package/dist/shared/session-field.d.ts +0 -3
- package/dist/shared/session-field.js +0 -6
|
@@ -6,14 +6,38 @@ import * as z from "zod";
|
|
|
6
6
|
function entryAsLabels(entry) {
|
|
7
7
|
return Object.fromEntries(Object.entries(entry).map(([locale, value]) => [locale, translationValueOtherText(value)]));
|
|
8
8
|
}
|
|
9
|
+
const labelsIndexByTranslations = new WeakMap();
|
|
10
|
+
/** Every tail starting at a key boundary (`:` or `.`, with and without the
|
|
11
|
+
* separator) maps to the labels of the first key that ends with it. */
|
|
12
|
+
function buildSuffixIndex(translations) {
|
|
13
|
+
const index = new Map();
|
|
14
|
+
for (const [key, entry] of Object.entries(translations)) {
|
|
15
|
+
const labels = entryAsLabels(entry);
|
|
16
|
+
const addTail = (start) => {
|
|
17
|
+
const tail = key.slice(start);
|
|
18
|
+
if (!index.has(tail))
|
|
19
|
+
index.set(tail, labels);
|
|
20
|
+
};
|
|
21
|
+
addTail(0);
|
|
22
|
+
for (let position = 0; position < key.length; position++) {
|
|
23
|
+
if (key[position] === ":" || key[position] === ".") {
|
|
24
|
+
addTail(position);
|
|
25
|
+
addTail(position + 1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return index;
|
|
30
|
+
}
|
|
9
31
|
/** Translation keys carry a feature prefix the manifest can't reconstruct
|
|
10
|
-
* (`showcase:entity:item:field:title`), so match on the suffix instead.
|
|
32
|
+
* (`showcase:entity:item:field:title`), so match on the suffix instead. The
|
|
33
|
+
* suffix must start at a `:` or `.` boundary of the key. */
|
|
11
34
|
function labelsForSuffix(translations, suffix) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
35
|
+
let index = labelsIndexByTranslations.get(translations);
|
|
36
|
+
if (index === undefined) {
|
|
37
|
+
index = buildSuffixIndex(translations);
|
|
38
|
+
labelsIndexByTranslations.set(translations, index);
|
|
15
39
|
}
|
|
16
|
-
return {};
|
|
40
|
+
return index.get(suffix) ?? {};
|
|
17
41
|
}
|
|
18
42
|
/** Nav/screen/workspace gating is opt-in: no rule means visible to everyone
|
|
19
43
|
* (NavDefinition.access). Handlers are the opposite — `hasAccess` is
|
|
@@ -205,6 +229,9 @@ function lastSegment(qualifiedId) {
|
|
|
205
229
|
const idx = qualifiedId.lastIndexOf(":");
|
|
206
230
|
return idx === -1 ? qualifiedId : qualifiedId.slice(idx + 1);
|
|
207
231
|
}
|
|
232
|
+
function detailIdParam(screen) {
|
|
233
|
+
return screen.type === "projectionDetail" ? (screen.idParam ?? "id") : "id";
|
|
234
|
+
}
|
|
208
235
|
function buildScreens(screens, navs, workspaces, translations, roles, visibleNavIds) {
|
|
209
236
|
const allNavs = [...navs.values()];
|
|
210
237
|
const result = [];
|
|
@@ -239,8 +266,8 @@ function buildScreens(screens, navs, workspaces, translations, roles, visibleNav
|
|
|
239
266
|
description: labelsForSuffix(translations, screen.description)["en"] ?? screen.description,
|
|
240
267
|
}),
|
|
241
268
|
...("entity" in screen && screen.entity !== undefined && { entity: screen.entity }),
|
|
242
|
-
// A detail screen takes the row id; everything else is parameterless.
|
|
243
|
-
params: screen.detailFor !== undefined ? [
|
|
269
|
+
// A detail screen takes the row id under its idParam; everything else is parameterless.
|
|
270
|
+
params: screen.detailFor !== undefined ? [detailIdParam(screen)] : [],
|
|
244
271
|
workspaces: screenWorkspaces,
|
|
245
272
|
...(screen.detailFor !== undefined && { detailFor: screen.detailFor }),
|
|
246
273
|
...("handler" in screen && typeof screen.handler === "string" && { handler: screen.handler }),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasAccess } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { hasAccess, normalizeAccessEntry } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { FILTER_OPS, isRecord } from "./filter-ops.js";
|
|
3
3
|
const MAX_TOOL_NAME_LENGTH = 64;
|
|
4
4
|
/** Field types that declare `filterable` (per `packages/framework/src/engine/types/fields.ts`).
|
|
@@ -74,14 +74,28 @@ function isDetailHandlerQn(qn, entityName) {
|
|
|
74
74
|
function isExplicitlyAgentHidden(def) {
|
|
75
75
|
return def.agent?.expose === false;
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
/** The model's filters feed the WHERE clause, and only the output is field-filtered — a field
|
|
78
|
+
* whose read access is role- or row-restricted would leak through `totalCount` and hit/miss
|
|
79
|
+
* probing. Only fields every one of these roles may read without a row condition are offered. */
|
|
80
|
+
function isReadableWithoutRowCondition(field, roles) {
|
|
81
|
+
const accessMap = normalizeAccessEntry(field.access?.read);
|
|
82
|
+
if (!accessMap || Object.keys(accessMap).length === 0)
|
|
83
|
+
return true;
|
|
84
|
+
return roles.every((role) => accessMap[role] === "all");
|
|
85
|
+
}
|
|
86
|
+
/** Entity CRUD tools are registry-derived, so an explicit `agent.risk` on the handler is read
|
|
87
|
+
* here; without a hint the read tools stay "low". */
|
|
88
|
+
function entityHandlerRisk(registry, qn) {
|
|
89
|
+
return registry.getAllQueryHandlers().get(qn)?.agent?.risk ?? "low";
|
|
90
|
+
}
|
|
91
|
+
function addToolsForListHandler(registry, qn, entityName, entity, roles, sink) {
|
|
78
92
|
const searchableFields = registry.getSearchableFields(entityName);
|
|
79
93
|
if (searchableFields.length > 0) {
|
|
80
94
|
const tool = buildSearchTool(entityName, searchableFields);
|
|
81
95
|
addTool(sink, tool, { kind: "search", entityName, qn });
|
|
82
96
|
}
|
|
83
97
|
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
84
|
-
if (!isFilterable(field))
|
|
98
|
+
if (!isFilterable(field) || !isReadableWithoutRowCondition(field, roles))
|
|
85
99
|
continue;
|
|
86
100
|
const fieldSchema = jsonSchemaTypeForField(field);
|
|
87
101
|
if (!fieldSchema)
|
|
@@ -98,7 +112,7 @@ function entityDisplayLabel(entity, entityName, locale) {
|
|
|
98
112
|
return label;
|
|
99
113
|
return entityName;
|
|
100
114
|
}
|
|
101
|
-
function buildGetTool(entityName, qn, label) {
|
|
115
|
+
function buildGetTool(entityName, qn, label, risk) {
|
|
102
116
|
return {
|
|
103
117
|
tool: {
|
|
104
118
|
name: `get_${entityName}`,
|
|
@@ -110,7 +124,7 @@ function buildGetTool(entityName, qn, label) {
|
|
|
110
124
|
additionalProperties: false,
|
|
111
125
|
},
|
|
112
126
|
},
|
|
113
|
-
descriptor: { kind: "server", op: "query", qn, risk
|
|
127
|
+
descriptor: { kind: "server", op: "query", qn, risk, entity: entityName, detail: true },
|
|
114
128
|
};
|
|
115
129
|
}
|
|
116
130
|
function buildListInputSchema(searchableFields, filterableFields) {
|
|
@@ -140,7 +154,7 @@ function buildListInputSchema(searchableFields, filterableFields) {
|
|
|
140
154
|
properties["limit"] = { type: "integer", minimum: 1, maximum: 200 };
|
|
141
155
|
return { type: "object", properties, required: [], additionalProperties: false };
|
|
142
156
|
}
|
|
143
|
-
function buildListTool(entityName, qn, label, searchableFields, filterableFields) {
|
|
157
|
+
function buildListTool(entityName, qn, label, searchableFields, filterableFields, risk) {
|
|
144
158
|
const descriptionParts = [`List ${label} records.`];
|
|
145
159
|
if (searchableFields.length > 0) {
|
|
146
160
|
descriptionParts.push(`Free-text search over: ${searchableFields.join(", ")}.`);
|
|
@@ -159,16 +173,22 @@ function buildListTool(entityName, qn, label, searchableFields, filterableFields
|
|
|
159
173
|
kind: "server",
|
|
160
174
|
op: "query",
|
|
161
175
|
qn,
|
|
162
|
-
risk
|
|
176
|
+
risk,
|
|
163
177
|
entity: entityName,
|
|
164
178
|
list: { searchableFields, filterableFields },
|
|
165
179
|
},
|
|
166
180
|
};
|
|
167
181
|
}
|
|
168
|
-
function filterableFieldsOf(entity) {
|
|
182
|
+
function filterableFieldsOf(entity, definition, roles) {
|
|
169
183
|
if (!entity)
|
|
170
184
|
return [];
|
|
171
|
-
return entity.fields
|
|
185
|
+
return entity.fields
|
|
186
|
+
.filter((field) => field.filterable === true)
|
|
187
|
+
.map((field) => field.name)
|
|
188
|
+
.filter((name) => {
|
|
189
|
+
const fieldDefinition = definition?.fields[name];
|
|
190
|
+
return fieldDefinition !== undefined && isReadableWithoutRowCondition(fieldDefinition, roles);
|
|
191
|
+
});
|
|
172
192
|
}
|
|
173
193
|
function compareByCodePoint(a, b) {
|
|
174
194
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -206,6 +226,25 @@ export function toolNameForQn(qn) {
|
|
|
206
226
|
const sanitized = joined.replace(/[^A-Za-z0-9_]/g, "_").replace(/_+/g, "_");
|
|
207
227
|
return sanitized.slice(0, MAX_TOOL_NAME_LENGTH);
|
|
208
228
|
}
|
|
229
|
+
// FNV-1a: only needs to be stable and well-spread, not collision-proof — a hash clash
|
|
230
|
+
// on top of a name clash falls through to the sink's drop-later-tool guard.
|
|
231
|
+
function shortQnHash(qn) {
|
|
232
|
+
let hash = 0x811c9dc5;
|
|
233
|
+
for (let index = 0; index < qn.length; index++) {
|
|
234
|
+
hash = Math.imul(hash ^ qn.charCodeAt(index), 0x01000193) >>> 0;
|
|
235
|
+
}
|
|
236
|
+
return hash.toString(16).padStart(8, "0");
|
|
237
|
+
}
|
|
238
|
+
/** Sanitizing, verb-dropping and truncation make distinct QNs share a tool name; the
|
|
239
|
+
* first QN in manifest order keeps the plain name, later ones get a QN-hash suffix so
|
|
240
|
+
* no handler silently disappears from the catalog. */
|
|
241
|
+
function uniqueToolNameForQn(qn, usedNames) {
|
|
242
|
+
const plainName = toolNameForQn(qn);
|
|
243
|
+
if (!usedNames.has(plainName))
|
|
244
|
+
return plainName;
|
|
245
|
+
const hashSuffix = `_${shortQnHash(qn)}`;
|
|
246
|
+
return `${plainName.slice(0, MAX_TOOL_NAME_LENGTH - hashSuffix.length)}${hashSuffix}`;
|
|
247
|
+
}
|
|
209
248
|
export const OPEN_FORM_TOOL_NAME = "open_form";
|
|
210
249
|
function buildNavigateTool(manifest) {
|
|
211
250
|
const entityScreens = new Map();
|
|
@@ -302,7 +341,7 @@ function addRegistrySearchTools(registry, roleFilter, denyQns, sink) {
|
|
|
302
341
|
const entity = registry.getEntity(entityName);
|
|
303
342
|
if (!entity)
|
|
304
343
|
continue;
|
|
305
|
-
addToolsForListHandler(registry, qn, entityName, entity, sink);
|
|
344
|
+
addToolsForListHandler(registry, qn, entityName, entity, roleFilter.roles, sink);
|
|
306
345
|
}
|
|
307
346
|
}
|
|
308
347
|
function collectEntityHandlerQns(registry, roleFilter, denyQns) {
|
|
@@ -333,21 +372,21 @@ function collectEntityHandlerQns(registry, roleFilter, denyQns) {
|
|
|
333
372
|
}
|
|
334
373
|
return { detailQnByEntity, listQnByEntity, entityListDetailQns };
|
|
335
374
|
}
|
|
336
|
-
function addGetTools(detailQnByEntity, entityByName, locale, sink) {
|
|
375
|
+
function addGetTools(registry, detailQnByEntity, entityByName, locale, sink) {
|
|
337
376
|
const getEntries = [...detailQnByEntity.entries()].sort((a, b) => compareByCodePoint(a[0], b[0]));
|
|
338
377
|
for (const [entityName, qn] of getEntries) {
|
|
339
378
|
const label = entityDisplayLabel(entityByName.get(entityName), entityName, locale);
|
|
340
|
-
const { tool, descriptor } = buildGetTool(entityName, qn, label);
|
|
379
|
+
const { tool, descriptor } = buildGetTool(entityName, qn, label, entityHandlerRisk(registry, qn));
|
|
341
380
|
addTool(sink, tool, descriptor);
|
|
342
381
|
}
|
|
343
382
|
}
|
|
344
|
-
function addListTools(registry, listQnByEntity, entityByName, locale, sink) {
|
|
383
|
+
function addListTools(registry, listQnByEntity, entityByName, roles, locale, sink) {
|
|
345
384
|
const listEntries = [...listQnByEntity.entries()].sort((a, b) => compareByCodePoint(a[0], b[0]));
|
|
346
385
|
for (const [entityName, qn] of listEntries) {
|
|
347
386
|
const label = entityDisplayLabel(entityByName.get(entityName), entityName, locale);
|
|
348
387
|
const searchableFields = registry.getSearchableFields(entityName);
|
|
349
|
-
const filterableFields = filterableFieldsOf(entityByName.get(entityName));
|
|
350
|
-
const { tool, descriptor } = buildListTool(entityName, qn, label, searchableFields, filterableFields);
|
|
388
|
+
const filterableFields = filterableFieldsOf(entityByName.get(entityName), registry.getEntity(entityName), roles);
|
|
389
|
+
const { tool, descriptor } = buildListTool(entityName, qn, label, searchableFields, filterableFields, entityHandlerRisk(registry, qn));
|
|
351
390
|
addTool(sink, tool, descriptor);
|
|
352
391
|
}
|
|
353
392
|
}
|
|
@@ -359,7 +398,7 @@ function addQueryHandlerTools(manifest, entityListDetailQns, denyQns, sink) {
|
|
|
359
398
|
continue;
|
|
360
399
|
if (entityListDetailQns.has(handler.qn))
|
|
361
400
|
continue;
|
|
362
|
-
const name =
|
|
401
|
+
const name = uniqueToolNameForQn(handler.qn, sink.usedNames);
|
|
363
402
|
addTool(sink, { name, description: handlerDescription(handler), inputSchema: handler.inputSchema }, {
|
|
364
403
|
kind: "server",
|
|
365
404
|
op: "query",
|
|
@@ -375,7 +414,7 @@ function addWriteHandlerTools(manifest, detailQnByEntity, denyQns, sink) {
|
|
|
375
414
|
continue;
|
|
376
415
|
if (denyQns.has(handler.qn))
|
|
377
416
|
continue;
|
|
378
|
-
const name =
|
|
417
|
+
const name = uniqueToolNameForQn(handler.qn, sink.usedNames);
|
|
379
418
|
const detailQn = handler.entity !== undefined ? detailQnByEntity.get(handler.entity) : undefined;
|
|
380
419
|
const injectsVersion = detailQn !== undefined && schemaRequiresField(handler.inputSchema, "version");
|
|
381
420
|
const inputSchema = injectsVersion
|
|
@@ -436,8 +475,8 @@ export function buildToolCatalog(registry, manifest, options) {
|
|
|
436
475
|
const denyQns = new Set(options.denyQns ?? []);
|
|
437
476
|
addRegistrySearchTools(registry, roleFilter, denyQns, sink);
|
|
438
477
|
const { detailQnByEntity, listQnByEntity, entityListDetailQns } = collectEntityHandlerQns(registry, roleFilter, denyQns);
|
|
439
|
-
addGetTools(detailQnByEntity, entityByName, locale, sink);
|
|
440
|
-
addListTools(registry, listQnByEntity, entityByName, locale, sink);
|
|
478
|
+
addGetTools(registry, detailQnByEntity, entityByName, locale, sink);
|
|
479
|
+
addListTools(registry, listQnByEntity, entityByName, roleFilter.roles, locale, sink);
|
|
441
480
|
addQueryHandlerTools(manifest, entityListDetailQns, denyQns, sink);
|
|
442
481
|
if (options.mode !== "read-only") {
|
|
443
482
|
addWriteHandlerTools(manifest, detailQnByEntity, denyQns, sink);
|
|
@@ -27,15 +27,19 @@ function buildDateRange(from, to) {
|
|
|
27
27
|
range.lte = Temporal.Instant.from(to);
|
|
28
28
|
return range;
|
|
29
29
|
}
|
|
30
|
+
function escapeLikePattern(value) {
|
|
31
|
+
return value.replace(/[\\%_]/g, (m) => `\\${m}`);
|
|
32
|
+
}
|
|
30
33
|
function buildAuditWhere(tenantId, p) {
|
|
31
34
|
const where = { tenantId };
|
|
32
35
|
if (p.aggregateType)
|
|
33
36
|
where["aggregateType"] = p.aggregateType;
|
|
34
37
|
if (p.aggregateId)
|
|
35
38
|
where["aggregateId"] = p.aggregateId;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
if (p.eventType)
|
|
40
|
+
where["type"] = p.eventType;
|
|
41
|
+
else if (p.search)
|
|
42
|
+
where["type"] = { like: `%${escapeLikePattern(p.search)}%` };
|
|
39
43
|
if (p.userId)
|
|
40
44
|
where["createdBy"] = p.userId;
|
|
41
45
|
const range = buildDateRange(p.from, p.to);
|
|
@@ -53,6 +53,7 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
|
|
|
53
53
|
access: { roles: ["anonymous"] },
|
|
54
54
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
55
55
|
escapeHatch: {
|
|
56
|
+
grants: ["unsafeRaw"],
|
|
56
57
|
reason: "Anonymous invite-accept has no session in the invited tenant yet — checks existing " +
|
|
57
58
|
"membership via ctx.queryAs(SYSTEM, tenant:query:memberships) of the invitation's tenant. " +
|
|
58
59
|
"Also reads the pending invitation by id (the invitee is not yet a member of the " +
|
|
@@ -51,6 +51,7 @@ export function createInviteAcceptHandler() {
|
|
|
51
51
|
},
|
|
52
52
|
agent: { expose: false },
|
|
53
53
|
escapeHatch: {
|
|
54
|
+
grants: ["unsafeRaw"],
|
|
54
55
|
reason: INVITE_ACCEPT_ESCAPE_HATCH_REASON,
|
|
55
56
|
},
|
|
56
57
|
// kumiko-lint-ignore complexity-budget invite branches (auth/anon/burn) stay in one handler
|
|
@@ -34,6 +34,7 @@ export const inviteInfoQuery = defineQueryHandler({
|
|
|
34
34
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
35
35
|
agent: { expose: false },
|
|
36
36
|
escapeHatch: {
|
|
37
|
+
grants: ["unsafeRaw"],
|
|
37
38
|
reason: READ_PENDING_INVITATION_REASON,
|
|
38
39
|
},
|
|
39
40
|
description: "Anonymous, read-only invite lookup for the invite-acceptance page: reveals the invited " +
|
|
@@ -51,6 +51,7 @@ export function createInviteSignupCompleteHandler() {
|
|
|
51
51
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
52
52
|
agent: { expose: false },
|
|
53
53
|
escapeHatch: {
|
|
54
|
+
grants: ["unsafeRaw", "globalWrites"],
|
|
54
55
|
reason: INVITE_SIGNUP_COMPLETE_ESCAPE_HATCH_REASON,
|
|
55
56
|
},
|
|
56
57
|
handler: async (event, ctx) => {
|
|
@@ -78,6 +78,7 @@ export function createSignupConfirmHandler() {
|
|
|
78
78
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
79
79
|
agent: { expose: false },
|
|
80
80
|
escapeHatch: {
|
|
81
|
+
grants: ["unsafeRaw", "systemIdentity"],
|
|
81
82
|
reason: SIGNUP_CONFIRM_PROVISION_REASON,
|
|
82
83
|
},
|
|
83
84
|
handler: async (event, ctx) => {
|
|
@@ -55,7 +55,7 @@ export function createSignupRequestHandler(opts) {
|
|
|
55
55
|
access: { roles: ["anonymous"] },
|
|
56
56
|
rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
|
|
57
57
|
description: "Starts magic-link self-registration by mailing a fresh activation link to an address and invalidating any link still outstanding for it; the answer looks the same whether or not the address is already registered.",
|
|
58
|
-
escapeHatch: { reason: HANDOVER_GRANT_VERIFY_REASON },
|
|
58
|
+
escapeHatch: { reason: HANDOVER_GRANT_VERIFY_REASON, grants: ["unsafeRaw"] },
|
|
59
59
|
handler: async (event, ctx) => {
|
|
60
60
|
// Silent no-op when off, matching the route's own always-200
|
|
61
61
|
// anti-enumeration contract (registerTokenRequestRoute swallows every
|
|
@@ -151,6 +151,12 @@ export async function seedAdmin(db, options) {
|
|
|
151
151
|
return { id: userId };
|
|
152
152
|
}
|
|
153
153
|
function compareInsertedAt(a, b) {
|
|
154
|
+
// selectMany<ActiveUserRow> is an unchecked row-shape claim. A codec that
|
|
155
|
+
// yields another type would make both comparisons false and silently fall
|
|
156
|
+
// back to id order, which can crown the younger duplicate as survivor.
|
|
157
|
+
if (typeof a.epochNanoseconds !== "bigint" || typeof b.epochNanoseconds !== "bigint") {
|
|
158
|
+
throw new Error("compareInsertedAt: user.insertedAt is not a Temporal.Instant");
|
|
159
|
+
}
|
|
154
160
|
if (a.epochNanoseconds < b.epochNanoseconds)
|
|
155
161
|
return -1;
|
|
156
162
|
if (a.epochNanoseconds > b.epochNanoseconds)
|
|
@@ -22,6 +22,9 @@ import { csrfHeader } from "./auth-client.js";
|
|
|
22
22
|
import { resolvePostAuthHref } from "./auth-form-logic.js";
|
|
23
23
|
import { AuthCard, useUrlToken } from "./auth-form-primitives.js";
|
|
24
24
|
import { SessionContext, UNAUTHENTICATED } from "./session.js";
|
|
25
|
+
function isMfaPending(data) {
|
|
26
|
+
return data.tenantId === undefined;
|
|
27
|
+
}
|
|
25
28
|
function grantedRoles(data) {
|
|
26
29
|
if (data.user?.roles !== undefined)
|
|
27
30
|
return data.user.roles;
|
|
@@ -46,7 +49,7 @@ export function InviteAcceptScreen({ title, token: tokenProp, loggedInHref = "/"
|
|
|
46
49
|
// challenge / setup requirement, but no session cookie was minted: send the
|
|
47
50
|
// user to the login screen (now a member) instead of a tenant URL.
|
|
48
51
|
const finishAccept = (data) => {
|
|
49
|
-
if (data
|
|
52
|
+
if (isMfaPending(data)) {
|
|
50
53
|
window.location.assign(loginHref);
|
|
51
54
|
return;
|
|
52
55
|
}
|
|
@@ -30,6 +30,7 @@ export function createEnableStartPreauthHandler(opts) {
|
|
|
30
30
|
// Same secret-bearing result as enable-start.
|
|
31
31
|
agent: { expose: false },
|
|
32
32
|
escapeHatch: {
|
|
33
|
+
grants: ["unsafeRaw"],
|
|
33
34
|
reason: ENABLE_START_PREAUTH_TENANT_REASON,
|
|
34
35
|
},
|
|
35
36
|
handler: async (event, ctx) => {
|
|
@@ -9,6 +9,18 @@ import { signMfaSetupToken } from "../mfa-setup-token.js";
|
|
|
9
9
|
import { buildOtpauthUri } from "../otpauth-uri.js";
|
|
10
10
|
import { generateRecoveryCodes, hashRecoveryCodes } from "../recovery-codes.js";
|
|
11
11
|
import { generateTotpSecret } from "../totp.js";
|
|
12
|
+
const FALLBACK_ACCOUNT_LABEL = "account";
|
|
13
|
+
// The authenticator app shows this label, so an internal user id must never leak into it.
|
|
14
|
+
function readableAccountLabel(me) {
|
|
15
|
+
if (typeof me !== "object" || me === null)
|
|
16
|
+
return FALLBACK_ACCOUNT_LABEL;
|
|
17
|
+
for (const field of ["email", "displayName"]) {
|
|
18
|
+
const value = Reflect.get(me, field);
|
|
19
|
+
if (typeof value === "string" && value.length > 0)
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
return FALLBACK_ACCOUNT_LABEL;
|
|
23
|
+
}
|
|
12
24
|
// Stateless setup: no `userMfa` row is created here. The generated secret +
|
|
13
25
|
// recovery-code hashes are signed into a short-lived `setupToken` (see
|
|
14
26
|
// mfa-setup-token.ts) that `enable-confirm` verifies. An abandoned setup
|
|
@@ -41,8 +53,7 @@ export function createEnableStartHandler(opts) {
|
|
|
41
53
|
if (!accountLabel) {
|
|
42
54
|
// The declarative secretMint screen has no client component left to
|
|
43
55
|
// read the session email off — derive the otpauth label server-side.
|
|
44
|
-
|
|
45
|
-
accountLabel = me?.email ?? event.user.id;
|
|
56
|
+
accountLabel = readableAccountLabel(await ctx.queryAs(event.user, UserQueries.me, {}));
|
|
46
57
|
}
|
|
47
58
|
const secret = generateTotpSecret();
|
|
48
59
|
const recoveryCodes = generateRecoveryCodes();
|
|
@@ -7,15 +7,15 @@ function hmacSign(input, secret) {
|
|
|
7
7
|
// minted for one MFA step from verifying as another (e.g. a preauth-setup token
|
|
8
8
|
// replayed as a challenge token), since both share the same secret and payload shape.
|
|
9
9
|
export function createHmacTokenCodec(domain, isPayload) {
|
|
10
|
-
function
|
|
10
|
+
function splitEncodedBody(value) {
|
|
11
11
|
if (typeof value !== "object" || value === null)
|
|
12
|
-
return
|
|
12
|
+
return null;
|
|
13
13
|
const v = value;
|
|
14
14
|
const expiresAtMs = v["expiresAtMs"];
|
|
15
15
|
if (typeof expiresAtMs !== "number")
|
|
16
|
-
return
|
|
16
|
+
return null;
|
|
17
17
|
const { expiresAtMs: _drop, ...rest } = v;
|
|
18
|
-
return isPayload(rest);
|
|
18
|
+
return isPayload(rest) ? { payload: rest, expiresAtMs } : null;
|
|
19
19
|
}
|
|
20
20
|
return {
|
|
21
21
|
sign(payload, ttlMinutes, secret, now = Temporal.Now.instant()) {
|
|
@@ -46,14 +46,13 @@ export function createHmacTokenCodec(domain, isPayload) {
|
|
|
46
46
|
catch {
|
|
47
47
|
return { ok: false, reason: "malformed" };
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
const body = splitEncodedBody(parsed);
|
|
50
|
+
if (!body)
|
|
50
51
|
return { ok: false, reason: "malformed" };
|
|
51
|
-
if (Temporal.Instant.compare(now, Temporal.Instant.fromEpochMilliseconds(
|
|
52
|
-
0) {
|
|
52
|
+
if (Temporal.Instant.compare(now, Temporal.Instant.fromEpochMilliseconds(body.expiresAtMs)) > 0) {
|
|
53
53
|
return { ok: false, reason: "expired" };
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
return { ok: true, payload: payload, expiresAtMs };
|
|
55
|
+
return { ok: true, payload: body.payload, expiresAtMs: body.expiresAtMs };
|
|
57
56
|
},
|
|
58
57
|
};
|
|
59
58
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// @runtime client
|
|
3
|
+
// Consumers that type-check this source never include the ambient declaration on their own.
|
|
4
|
+
/// <reference path="./qrcode-browser.d.ts" />
|
|
3
5
|
// Pre-auth twin of the declarative auth-mfa-enable secretMint screen: a user
|
|
4
6
|
// blocked at login by MFA enforcement and not yet enrolled lands here
|
|
5
7
|
// straight out of the login flow — there is no session yet. Identity comes
|
|
@@ -18,3 +18,5 @@ export declare function paymentAggregateId(tenantId: string): string;
|
|
|
18
18
|
* providerEventId)` rather than the event-store's bigserial `event.id` (not
|
|
19
19
|
* a UUID) or the shared per-tenant aggregateId (one row per payment). */
|
|
20
20
|
export declare function paymentRowId(tenantId: string, providerName: string, providerEventId: string): string;
|
|
21
|
+
/** Stream id of one unmatched termination declaration, derived from its request id. */
|
|
22
|
+
export declare function terminationUnmatchedAggregateId(requestId: string): string;
|
|
@@ -16,6 +16,9 @@ const PAYMENT_NAMESPACE = "dec0b897-646d-4da3-be3c-e9a35a83f4e0";
|
|
|
16
16
|
* UUID, so it can't back a uuid-typed PK. Generated 2026-09-11, set in
|
|
17
17
|
* stone (same rationale as the two namespaces above). */
|
|
18
18
|
const PAYMENT_ROW_NAMESPACE = "3a1c9f4e-6b2d-4c8a-9e5f-7d4b1a2c8e6f";
|
|
19
|
+
/** Namespace for the system-tenant stream of an unmatched termination
|
|
20
|
+
* declaration (one stream per request). Generated 2026-10-03, set in stone. */
|
|
21
|
+
const TERMINATION_UNMATCHED_NAMESPACE = "7e2f4b9a-3c1d-4a68-8d05-b6c9e1f27a34";
|
|
19
22
|
/**
|
|
20
23
|
* Deterministic aggregate-id für die subscription eines Plattform-
|
|
21
24
|
* Tenants. EINE Subscription pro Tenant (Add-Ons sind line-items in
|
|
@@ -51,3 +54,8 @@ export function paymentRowId(tenantId, providerName, providerEventId) {
|
|
|
51
54
|
// the payment with no error anywhere.
|
|
52
55
|
return uuidv5(`${tenantId}:${providerName}:${providerEventId}`, PAYMENT_ROW_NAMESPACE);
|
|
53
56
|
}
|
|
57
|
+
/** Stream id of one unmatched termination declaration, derived from its request id. */
|
|
58
|
+
// @wrapper-known uuid-domain
|
|
59
|
+
export function terminationUnmatchedAggregateId(requestId) {
|
|
60
|
+
return uuidv5(requestId, TERMINATION_UNMATCHED_NAMESPACE);
|
|
61
|
+
}
|
|
@@ -88,6 +88,9 @@ export type OpenCheckoutInput = {
|
|
|
88
88
|
readonly cancelUrl: string;
|
|
89
89
|
readonly providerCustomerId?: string;
|
|
90
90
|
readonly mode?: "subscription" | "payment";
|
|
91
|
+
readonly consentId?: string;
|
|
92
|
+
readonly locale?: string;
|
|
93
|
+
readonly submitMessage?: string;
|
|
91
94
|
};
|
|
92
95
|
/** The one checkout entry-point create-checkout-session and
|
|
93
96
|
* start-plan-checkout both funnel through. `mode: "payment"` (one-off
|
|
@@ -282,6 +282,9 @@ export async function openCheckout(ctx, options, input) {
|
|
|
282
282
|
// pre-hardening wire-contract for callers that never sent `mode`
|
|
283
283
|
// (create-checkout-session's own regression pin).
|
|
284
284
|
...(input.mode && { mode: input.mode }),
|
|
285
|
+
...(input.consentId && { consentId: input.consentId }),
|
|
286
|
+
...(input.locale && { locale: input.locale }),
|
|
287
|
+
...(input.submitMessage && { submitMessage: input.submitMessage }),
|
|
285
288
|
});
|
|
286
289
|
return { url: result.url, providerName: input.providerName };
|
|
287
290
|
}
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
3
3
|
export declare const BILLING_FOUNDATION_FEATURE: "billing-foundation";
|
|
4
4
|
export declare const SUBSCRIPTION_PROVIDER_EXTENSION: "subscriptionProvider";
|
|
5
|
+
/** delivery notification type of the § 312f contract confirmation mail. */
|
|
6
|
+
export declare const CONTRACT_CONFIRMATION_NOTIFICATION_TYPE: "billing-foundation:contract-confirmation";
|
|
7
|
+
/** delivery notification types of the § 312k termination receipt and the
|
|
8
|
+
* operator notice. */
|
|
9
|
+
export declare const CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE: "billing-foundation:contract-termination-receipt";
|
|
10
|
+
export declare const CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE: "billing-foundation:contract-termination-operator-notice";
|
|
5
11
|
export declare const SubscriptionFoundationHandlers: {
|
|
6
12
|
/** Programmatic entry-point für den webhook-handler. Receives the
|
|
7
13
|
* parsed SubscriptionEvent (vom Plugin schon verifiziert) + macht
|
|
@@ -22,6 +28,22 @@ export declare const SubscriptionFoundationHandlers: {
|
|
|
22
28
|
* (payment-aggregate), separate from the subscription-aggregate — a
|
|
23
29
|
* payment is not a subscription-state transition. */
|
|
24
30
|
readonly processPaymentEvent: "billing-foundation:write:process-payment-event";
|
|
31
|
+
/** System-only: sends the § 312f contract confirmation mail for a recorded
|
|
32
|
+
* checkout consent (consumerProtection only). */
|
|
33
|
+
readonly issueContractConfirmation: "billing-foundation:write:issue-contract-confirmation";
|
|
34
|
+
/** System-only: records a termination/withdrawal on the tenant's
|
|
35
|
+
* subscription stream and asks the provider to cancel (consumerProtection
|
|
36
|
+
* only). */
|
|
37
|
+
readonly recordContractTermination: "billing-foundation:write:record-contract-termination";
|
|
38
|
+
/** System-only: records a public declaration no single tenant could be
|
|
39
|
+
* matched to, on a system-tenant stream (consumerProtection only). */
|
|
40
|
+
readonly recordUnmatchedContractTermination: "billing-foundation:write:record-unmatched-contract-termination";
|
|
41
|
+
/** Anonymous § 312k cancellation/withdrawal declaration (consumerProtection
|
|
42
|
+
* only). */
|
|
43
|
+
readonly requestContractTermination: "billing-foundation:write:request-contract-termination";
|
|
44
|
+
/** Tenant-Admin terminates or withdraws from the contract of their own
|
|
45
|
+
* tenant (consumerProtection only). */
|
|
46
|
+
readonly terminateContract: "billing-foundation:write:terminate-contract";
|
|
25
47
|
/** Tenant-Admin/purchase-role picks a plan tier from the catalog with no
|
|
26
48
|
* existing non-terminal subscription — starts a hosted checkout for the
|
|
27
49
|
* matching price. Only registered when `createBillingFoundationFeature`
|
|
@@ -47,6 +69,9 @@ export declare const SubscriptionFoundationQueries: {
|
|
|
47
69
|
* current tier and per-plan action. Only registered when a `catalog` is
|
|
48
70
|
* configured. */
|
|
49
71
|
readonly billingPlans: "billing-foundation:query:billing-plans";
|
|
72
|
+
/** System-only probe: does the caller tenant hold a non-terminal
|
|
73
|
+
* subscription (consumerProtection only). */
|
|
74
|
+
readonly terminableSubscription: "billing-foundation:query:terminable-subscription";
|
|
50
75
|
};
|
|
51
76
|
export declare const SubscriptionEventTypes: {
|
|
52
77
|
readonly created: "subscription.created";
|
|
@@ -69,6 +94,12 @@ export declare const BillingEventKinds: {
|
|
|
69
94
|
readonly payment: "payment";
|
|
70
95
|
};
|
|
71
96
|
export type BillingEventKind = (typeof BillingEventKinds)[keyof typeof BillingEventKinds];
|
|
97
|
+
export declare const SubscriptionCancelTimings: {
|
|
98
|
+
readonly periodEnd: "period-end";
|
|
99
|
+
readonly immediately: "immediately";
|
|
100
|
+
};
|
|
101
|
+
export type SubscriptionCancelTiming = (typeof SubscriptionCancelTimings)[keyof typeof SubscriptionCancelTimings];
|
|
102
|
+
export declare function parseProviderConsentId(raw: unknown): string | undefined;
|
|
72
103
|
export declare const BILLING_PLANS_SCREEN_ID: "billing-plans";
|
|
73
104
|
export declare const BILLING_PLANS_PANEL_COMPONENT: "BillingPlansPanel";
|
|
74
105
|
export declare const BillingPlanActions: {
|
|
@@ -12,6 +12,12 @@ export const BILLING_FOUNDATION_FEATURE = "billing-foundation";
|
|
|
12
12
|
// Extension-point name fuer Provider-Plugins (subscription-stripe,
|
|
13
13
|
// subscription-mollie, ...).
|
|
14
14
|
export const SUBSCRIPTION_PROVIDER_EXTENSION = "subscriptionProvider";
|
|
15
|
+
/** delivery notification type of the § 312f contract confirmation mail. */
|
|
16
|
+
export const CONTRACT_CONFIRMATION_NOTIFICATION_TYPE = "billing-foundation:contract-confirmation";
|
|
17
|
+
/** delivery notification types of the § 312k termination receipt and the
|
|
18
|
+
* operator notice. */
|
|
19
|
+
export const CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE = "billing-foundation:contract-termination-receipt";
|
|
20
|
+
export const CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE = "billing-foundation:contract-termination-operator-notice";
|
|
15
21
|
// Qualified write handler names (QN format: scope:type:name).
|
|
16
22
|
export const SubscriptionFoundationHandlers = {
|
|
17
23
|
/** Programmatic entry-point für den webhook-handler. Receives the
|
|
@@ -33,6 +39,22 @@ export const SubscriptionFoundationHandlers = {
|
|
|
33
39
|
* (payment-aggregate), separate from the subscription-aggregate — a
|
|
34
40
|
* payment is not a subscription-state transition. */
|
|
35
41
|
processPaymentEvent: "billing-foundation:write:process-payment-event",
|
|
42
|
+
/** System-only: sends the § 312f contract confirmation mail for a recorded
|
|
43
|
+
* checkout consent (consumerProtection only). */
|
|
44
|
+
issueContractConfirmation: "billing-foundation:write:issue-contract-confirmation",
|
|
45
|
+
/** System-only: records a termination/withdrawal on the tenant's
|
|
46
|
+
* subscription stream and asks the provider to cancel (consumerProtection
|
|
47
|
+
* only). */
|
|
48
|
+
recordContractTermination: "billing-foundation:write:record-contract-termination",
|
|
49
|
+
/** System-only: records a public declaration no single tenant could be
|
|
50
|
+
* matched to, on a system-tenant stream (consumerProtection only). */
|
|
51
|
+
recordUnmatchedContractTermination: "billing-foundation:write:record-unmatched-contract-termination",
|
|
52
|
+
/** Anonymous § 312k cancellation/withdrawal declaration (consumerProtection
|
|
53
|
+
* only). */
|
|
54
|
+
requestContractTermination: "billing-foundation:write:request-contract-termination",
|
|
55
|
+
/** Tenant-Admin terminates or withdraws from the contract of their own
|
|
56
|
+
* tenant (consumerProtection only). */
|
|
57
|
+
terminateContract: "billing-foundation:write:terminate-contract",
|
|
36
58
|
/** Tenant-Admin/purchase-role picks a plan tier from the catalog with no
|
|
37
59
|
* existing non-terminal subscription — starts a hosted checkout for the
|
|
38
60
|
* matching price. Only registered when `createBillingFoundationFeature`
|
|
@@ -59,6 +81,9 @@ export const SubscriptionFoundationQueries = {
|
|
|
59
81
|
* current tier and per-plan action. Only registered when a `catalog` is
|
|
60
82
|
* configured. */
|
|
61
83
|
billingPlans: "billing-foundation:query:billing-plans",
|
|
84
|
+
/** System-only probe: does the caller tenant hold a non-terminal
|
|
85
|
+
* subscription (consumerProtection only). */
|
|
86
|
+
terminableSubscription: "billing-foundation:query:terminable-subscription",
|
|
62
87
|
};
|
|
63
88
|
// Normalized subscription-event types — provider-agnostic.
|
|
64
89
|
// Alle Provider-Plugins normalisieren ihre eigenen event-types auf einen
|
|
@@ -90,6 +115,16 @@ export const BillingEventKinds = {
|
|
|
90
115
|
subscription: "subscription",
|
|
91
116
|
payment: "payment",
|
|
92
117
|
};
|
|
118
|
+
export const SubscriptionCancelTimings = {
|
|
119
|
+
periodEnd: "period-end",
|
|
120
|
+
immediately: "immediately",
|
|
121
|
+
};
|
|
122
|
+
// consentId is echoed back by the provider (checkout metadata), so it is
|
|
123
|
+
// untrusted input: bounded length, id-ish charset only.
|
|
124
|
+
const CONSENT_ID_PATTERN = /^[A-Za-z0-9_-]{1,100}$/;
|
|
125
|
+
export function parseProviderConsentId(raw) {
|
|
126
|
+
return typeof raw === "string" && CONSENT_ID_PATTERN.test(raw) ? raw : undefined;
|
|
127
|
+
}
|
|
93
128
|
// Billing-plans screen/panel identifiers — the dormant dashboard app-builders
|
|
94
129
|
// mount their own catalog-derived nav entry onto.
|
|
95
130
|
export const BILLING_PLANS_SCREEN_ID = "billing-plans";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CheckoutConsentRecordedPayload } from "../events.js";
|
|
2
|
+
export type ContractConfirmationContent = {
|
|
3
|
+
readonly subject: string;
|
|
4
|
+
readonly header: string;
|
|
5
|
+
readonly sections: readonly {
|
|
6
|
+
readonly text: string;
|
|
7
|
+
}[];
|
|
8
|
+
readonly footer: string;
|
|
9
|
+
};
|
|
10
|
+
export type RenderContractConfirmationArgs = {
|
|
11
|
+
readonly consent: CheckoutConsentRecordedPayload;
|
|
12
|
+
readonly consentGivenAtIso: string;
|
|
13
|
+
readonly contractStartIso: string;
|
|
14
|
+
readonly currentPeriodEndIso?: string;
|
|
15
|
+
readonly vatNote: Readonly<Record<string, string>>;
|
|
16
|
+
readonly operatorEmail: string;
|
|
17
|
+
readonly termsContent: string;
|
|
18
|
+
};
|
|
19
|
+
/** Structured mail content (header/sections/footer) for the email channel's
|
|
20
|
+
* renderer; the renderer owns HTML escaping, this only builds plain text. */
|
|
21
|
+
export declare function renderContractConfirmation(args: RenderContractConfirmationArgs): ContractConfirmationContent;
|