@cosmicdrift/kumiko-bundled-features 0.365.1 → 0.367.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/admin-shell/feature.js +9 -9
- package/dist/agent-tools/tool-dispatch.d.ts +9 -1
- package/dist/agent-tools/tool-dispatch.js +55 -5
- package/dist/audit/feature.js +1 -1
- package/dist/auth-email-password/constants.d.ts +0 -1
- package/dist/auth-email-password/constants.js +0 -2
- package/dist/auth-email-password/feature.js +3 -3
- package/dist/auth-email-password/handlers/invite-create.write.js +1 -0
- package/dist/auth-mfa/feature.js +4 -4
- package/dist/auth-mfa-user-data/index.js +1 -1
- package/dist/billing-foundation/consumer-protection/register-consent.js +1 -1
- package/dist/billing-foundation/feature.js +1 -1
- package/dist/cap-counter/cap-reservation.js +73 -25
- package/dist/cap-counter/constants.d.ts +1 -0
- package/dist/cap-counter/constants.js +2 -0
- package/dist/cap-counter/feature.js +2 -2
- package/dist/cap-counter/tables.js +5 -2
- package/dist/cap-overview/feature.js +3 -3
- package/dist/channel-discord/feature.js +2 -2
- package/dist/channel-email/email-channel.d.ts +5 -1
- package/dist/channel-email/email-channel.js +8 -2
- package/dist/channel-email/feature.js +2 -2
- package/dist/channel-in-app/feature.js +1 -1
- package/dist/channel-push/feature.js +1 -1
- package/dist/channel-slack/feature.js +2 -2
- package/dist/channel-teams/feature.js +2 -2
- package/dist/channel-telegram/feature.js +2 -2
- package/dist/compliance-profiles/feature.js +1 -1
- package/dist/compliance-profiles-ops/index.js +3 -3
- package/dist/config/feature.js +1 -1
- package/dist/custom-fields/feature.js +2 -2
- package/dist/custom-fields/wire-for-entity.js +17 -13
- package/dist/data-retention/feature.js +1 -1
- package/dist/data-retention/presets.d.ts +4 -4
- package/dist/delivery/channel-context.js +1 -0
- package/dist/delivery/db/queries/address-opt-outs.d.ts +1 -1
- package/dist/delivery/db/queries/address-opt-outs.js +6 -2
- package/dist/delivery/db/queries/preferences.d.ts +1 -1
- package/dist/delivery/db/queries/preferences.js +4 -4
- package/dist/delivery/delivery-service.d.ts +2 -0
- package/dist/delivery/delivery-service.js +39 -6
- package/dist/delivery/feature.js +10 -6
- package/dist/delivery/handlers/resubscribe-address.write.js +13 -9
- package/dist/delivery/handlers/resubscribe-user.write.js +3 -1
- package/dist/delivery/handlers/set-preference.write.js +3 -1
- package/dist/delivery/handlers/unsubscribe-address.write.d.ts +6 -0
- package/dist/delivery/handlers/unsubscribe-address.write.js +11 -6
- package/dist/delivery/handlers/unsubscribe-user.write.js +3 -1
- package/dist/delivery/index.d.ts +3 -0
- package/dist/delivery/index.js +2 -0
- package/dist/delivery/notification-type-aliases.d.ts +3 -0
- package/dist/delivery/notification-type-aliases.js +11 -0
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/types.d.ts +2 -1
- package/dist/delivery/unsubscribe-link.d.ts +25 -0
- package/dist/delivery/unsubscribe-link.js +40 -0
- package/dist/delivery/unsubscribe-page-strings.d.ts +11 -0
- package/dist/delivery/unsubscribe-page-strings.js +15 -0
- package/dist/delivery/unsubscribe.d.ts +3 -0
- package/dist/delivery/unsubscribe.js +65 -29
- package/dist/derivatives-sharp/feature.js +1 -1
- package/dist/document-ingest-foundation/feature.js +2 -2
- package/dist/file-derivatives/feature.js +3 -3
- package/dist/file-foundation/feature.js +1 -1
- package/dist/file-provider-inmemory/feature.js +2 -2
- package/dist/file-provider-s3/feature.js +3 -3
- package/dist/file-provider-s3-env/feature.js +2 -2
- package/dist/files-tenant-data/index.js +1 -1
- package/dist/folders/feature.js +2 -2
- package/dist/folders-user-data/index.js +1 -1
- package/dist/form-draft/feature.js +2 -2
- package/dist/form-draft-user-data/index.js +1 -1
- package/dist/inbound-mail-foundation/connect-routes.d.ts +3 -0
- package/dist/inbound-mail-foundation/connect-routes.js +6 -1
- package/dist/inbound-mail-foundation/feature.js +1 -1
- package/dist/inbound-mail-foundation/types.d.ts +17 -7
- package/dist/inbound-mail-foundation/watch-supervisor.js +16 -2
- package/dist/inbound-provider-imap/feature.js +2 -2
- package/dist/inbound-provider-inmemory/feature.js +1 -1
- package/dist/ledger/feature.js +5 -5
- package/dist/legal-pages/feature.js +1 -1
- package/dist/mail-foundation/feature.d.ts +16 -1
- package/dist/mail-foundation/feature.js +30 -9
- package/dist/mail-foundation/index.d.ts +1 -1
- package/dist/mail-foundation/index.js +1 -1
- package/dist/mail-transport-inmemory/feature.js +2 -2
- package/dist/mail-transport-smtp/feature.js +3 -3
- package/dist/managed-pages/css-gate.js +1 -1
- package/dist/managed-pages/feature.js +2 -2
- package/dist/metrics/feature.js +2 -2
- package/dist/notes-history/feature.js +2 -2
- package/dist/notes-history-user-data/index.js +1 -1
- package/dist/personal-access-tokens/feature.js +17 -13
- package/dist/readiness/feature.js +2 -2
- package/dist/renderer-foundation/feature.js +1 -1
- package/dist/renderer-pdf/feature.js +1 -1
- package/dist/secrets/feature.js +1 -1
- package/dist/seo/feature.js +1 -1
- package/dist/sessions/feature.js +20 -16
- package/dist/step-dispatcher/feature.js +1 -1
- package/dist/subscription-mollie/feature.js +1 -1
- package/dist/subscription-stripe/feature.js +3 -3
- package/dist/tags/feature.js +2 -2
- package/dist/template-resolver/compile-document-body.js +47 -11
- package/dist/template-resolver/document-sanitizer.d.ts +2 -0
- package/dist/template-resolver/document-sanitizer.js +16 -1
- package/dist/template-resolver/document-schema.d.ts +2 -0
- package/dist/template-resolver/document-schema.js +31 -0
- package/dist/template-resolver/feature.js +2 -2
- package/dist/template-resolver/handlers/bundle-import.js +3 -2
- package/dist/template-resolver/handlers/copy-locale.write.js +3 -2
- package/dist/template-resolver/handlers/find-by-id.query.js +5 -1
- package/dist/template-resolver/handlers/list.query.js +3 -1
- package/dist/template-resolver/handlers/overview.query.js +4 -2
- package/dist/template-resolver/handlers/template-write.d.ts +2 -1
- package/dist/template-resolver/handlers/template-write.js +12 -1
- package/dist/template-resolver/index.d.ts +3 -2
- package/dist/template-resolver/index.js +3 -2
- package/dist/template-resolver/qualified-names.d.ts +1 -0
- package/dist/template-resolver/qualified-names.js +1 -0
- package/dist/template-resolver/seeding.d.ts +4 -0
- package/dist/template-resolver/seeding.js +40 -5
- package/dist/template-resolver/template-preview.js +2 -0
- package/dist/template-resolver/template-system-only.d.ts +8 -0
- package/dist/template-resolver/template-system-only.js +11 -0
- package/dist/template-resolver/template-validation.js +42 -28
- package/dist/template-resolver/web/i18n.js +1 -0
- package/dist/template-resolver-user-data/index.js +1 -1
- package/dist/tenant/feature.js +4 -4
- package/dist/tenant/seeding.js +1 -1
- package/dist/tenant-lifecycle/feature.js +2 -3
- package/dist/tenant-settings/feature.js +1 -1
- package/dist/tier-engine/feature.js +86 -72
- package/dist/user/feature.js +1 -1
- package/dist/user-data-rights/feature.js +4 -4
- package/dist/user-data-rights-defaults/feature.js +2 -2
- package/dist/user-profile/feature.js +4 -4
- package/dist/waitlist/feature.js +2 -2
- package/package.json +10 -10
- package/src/agent-tools/changes.json +6 -0
- package/src/auth-email-password/changes.json +13 -0
- package/src/cap-counter/changes.json +7 -0
- package/src/channel-email/changes.json +6 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/delivery/changes.json +18 -0
- package/src/inbound-mail-foundation/changes.json +13 -0
- package/src/template-resolver/changes.json +25 -0
- package/src/user-data-rights/changes.json +6 -0
package/dist/ledger/feature.js
CHANGED
|
@@ -28,9 +28,9 @@ function registerLedger(r, access, toggleable) {
|
|
|
28
28
|
});
|
|
29
29
|
if (toggleable !== undefined)
|
|
30
30
|
r.toggleable(toggleable);
|
|
31
|
-
r.entity("account", accountEntity);
|
|
32
|
-
r.entity("transaction", transactionEntity);
|
|
33
|
-
r.entity("schedule", scheduleEntity);
|
|
31
|
+
r.entity({ name: "account", ...accountEntity });
|
|
32
|
+
r.entity({ name: "transaction", ...transactionEntity });
|
|
33
|
+
r.entity({ name: "schedule", ...scheduleEntity });
|
|
34
34
|
// Chart of accounts — plain CRUD, no custom logic. No delete in v1: removing an
|
|
35
35
|
// account that has postings would orphan them; a posting-aware guard lands with
|
|
36
36
|
// the postings projection (Phase 1).
|
|
@@ -73,8 +73,8 @@ function registerLedger(r, access, toggleable) {
|
|
|
73
73
|
access,
|
|
74
74
|
description: "Changes a recurring booking template's description, accounts, amount, interval or date window; use it when a standing order's terms change, already confirmed periods stay booked as they were.",
|
|
75
75
|
}));
|
|
76
|
-
r.hook("preSave", "schedule:create", requireScheduleSubjectPair);
|
|
77
|
-
r.hook("preSave", "schedule:update", requireScheduleSubjectPair);
|
|
76
|
+
r.hook({ type: "preSave", target: "schedule:create", handler: requireScheduleSubjectPair });
|
|
77
|
+
r.hook({ type: "preSave", target: "schedule:update", handler: requireScheduleSubjectPair });
|
|
78
78
|
r.writeHandler(createConfirmSchedulePeriodHandler(access));
|
|
79
79
|
r.queryHandler(defineEntityListHandler("schedule", scheduleEntity, {
|
|
80
80
|
access,
|
|
@@ -35,7 +35,7 @@ export function createLegalPagesFeature(opts = {}) {
|
|
|
35
35
|
category: "content",
|
|
36
36
|
recommended: false,
|
|
37
37
|
});
|
|
38
|
-
r.requires("template-resolver");
|
|
38
|
+
r.requires({ features: ["template-resolver"] });
|
|
39
39
|
// Public HTML routes (default: 4 DACH routes, configurable via opts.routes)
|
|
40
40
|
for (const route of routes) {
|
|
41
41
|
r.httpRoute({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EmailTransport } from "@cosmicdrift/kumiko-bundled-features/channel-email";
|
|
2
|
-
import { type ConfigAccessor, type Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { type ConfigAccessor, type Registry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
export declare const MAIL_TRANSPORT_EXTENSION: "mailTransport";
|
|
4
4
|
/**
|
|
5
5
|
* Schmaler Surface-Type fuer Transport-Plugins — gespiegelt von
|
|
@@ -69,3 +69,18 @@ export declare const mailFoundationFeature: import("@cosmicdrift/kumiko-types/fe
|
|
|
69
69
|
* await transport.send({ to, subject, html });
|
|
70
70
|
*/
|
|
71
71
|
export declare function createTransportForTenant(ctx: MailTransportContext, tenantId: string, handlerName?: string): Promise<EmailTransport>;
|
|
72
|
+
/**
|
|
73
|
+
* Like createTransportForTenant, but an empty `provider` key means "this tenant has no
|
|
74
|
+
* mail provider" and yields null. Every other problem (unknown provider, plugin build
|
|
75
|
+
* failure) still throws, so a configured tenant never silently falls back.
|
|
76
|
+
*/
|
|
77
|
+
export declare function createTransportForTenantIfConfigured(ctx: MailTransportContext, tenantId: string, handlerName?: string): Promise<EmailTransport | null>;
|
|
78
|
+
export type TenantMailTransportScope = {
|
|
79
|
+
readonly tenantId: TenantId;
|
|
80
|
+
readonly config?: ConfigAccessor | undefined;
|
|
81
|
+
readonly registry: Registry;
|
|
82
|
+
readonly secrets?: import("@cosmicdrift/kumiko-framework/secrets").SecretsContext | undefined;
|
|
83
|
+
};
|
|
84
|
+
/** The `tenantTransport` resolver for `createChannelEmailFeature`: the tenant's configured
|
|
85
|
+
* provider, or null so the channel falls back to its app transport. */
|
|
86
|
+
export declare function tenantMailTransport(scope: TenantMailTransportScope): Promise<EmailTransport | null>;
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// — additive Feature.
|
|
33
33
|
import { withPiiCiphertextGuard, } from "@cosmicdrift/kumiko-bundled-features/channel-email";
|
|
34
34
|
import { requireDefined } from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
|
|
35
|
-
import { access, createTenantConfig, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
|
|
35
|
+
import { access, createSystemUser, createTenantConfig, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
|
|
36
36
|
const FEATURE_NAME = "mail-foundation";
|
|
37
37
|
// Extension-point name for transport-provider plugins (mail-transport-smtp,
|
|
38
38
|
// mail-transport-brevo-api, ...).
|
|
@@ -46,13 +46,13 @@ export function isMailTransportPlugin(o) {
|
|
|
46
46
|
// Feature-definition
|
|
47
47
|
// =============================================================================
|
|
48
48
|
export const mailFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
49
|
-
r.describe("Defines the `mailTransport` extension point and a per-tenant `provider` config key that selects which registered transport plugin to use at runtime. Call `createTransportForTenant(ctx, tenantId)` to get an `EmailTransport` ready for sending \u2014 use this feature together with at least one `mail-transport-*` feature; use `delivery` + `channel-email` instead when you need the full notification pipeline with delivery attempts and user preferences.");
|
|
49
|
+
r.describe("Defines the `mailTransport` extension point and a per-tenant `provider` config key that selects which registered transport plugin to use at runtime. Call `createTransportForTenant(ctx, tenantId)` to get an `EmailTransport` ready for sending \u2014 use this feature together with at least one `mail-transport-*` feature; use `delivery` + `channel-email` instead when you need the full notification pipeline with delivery attempts and user preferences. Pass `tenantMailTransport` as `tenantTransport` to `createChannelEmailFeature` so a tenant with a provider configured sends delivery mails through its own transport and other tenants through the app transport.");
|
|
50
50
|
r.uiHints({
|
|
51
51
|
displayLabel: "Mail Transport Foundation",
|
|
52
52
|
category: "notifications",
|
|
53
53
|
recommended: false,
|
|
54
54
|
});
|
|
55
|
-
r.requires("config");
|
|
55
|
+
r.requires({ features: ["config"] });
|
|
56
56
|
// Plugin extension-point. Provider-features register here. The
|
|
57
57
|
// entityName at registration time becomes the value tenants pick in
|
|
58
58
|
// `provider` config-key (e.g. "smtp", "brevo-api").
|
|
@@ -99,6 +99,21 @@ export const mailFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
99
99
|
* await transport.send({ to, subject, html });
|
|
100
100
|
*/
|
|
101
101
|
export async function createTransportForTenant(ctx, tenantId, handlerName = "mail-foundation:transport-factory") {
|
|
102
|
+
const transport = await createTransportForTenantIfConfigured(ctx, tenantId, handlerName);
|
|
103
|
+
if (transport === null) {
|
|
104
|
+
const usages = ctx.registry?.getExtensionUsages(MAIL_TRANSPORT_EXTENSION) ?? [];
|
|
105
|
+
const known = usages.map((u) => u.entityName).join(", ") || "<none>";
|
|
106
|
+
throw new Error(`${FEATURE_NAME}: no provider selected — set the 'provider' config-key to one of: ${known}. ` +
|
|
107
|
+
`Mount a mail-transport-* feature first if no plugins are registered.`);
|
|
108
|
+
}
|
|
109
|
+
return transport;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Like createTransportForTenant, but an empty `provider` key means "this tenant has no
|
|
113
|
+
* mail provider" and yields null. Every other problem (unknown provider, plugin build
|
|
114
|
+
* failure) still throws, so a configured tenant never silently falls back.
|
|
115
|
+
*/
|
|
116
|
+
export async function createTransportForTenantIfConfigured(ctx, tenantId, handlerName = "mail-foundation:transport-factory") {
|
|
102
117
|
const ctxConfig = ctx.config;
|
|
103
118
|
if (!ctxConfig) {
|
|
104
119
|
throw new Error(`${handlerName}: ctx.config is missing — feature requires the config-feature mounted in the registry`);
|
|
@@ -107,12 +122,8 @@ export async function createTransportForTenant(ctx, tenantId, handlerName = "mai
|
|
|
107
122
|
throw new Error(`${handlerName}: ctx.registry is missing — required to look up registered mail-transport plugins`);
|
|
108
123
|
}
|
|
109
124
|
const provider = requireDefined(await ctxConfig(mailFoundationFeature.exports.providerConfigKey), FEATURE_NAME, "provider"); // @cast-boundary engine-payload
|
|
110
|
-
if (provider.length === 0)
|
|
111
|
-
|
|
112
|
-
const known = usages.map((u) => u.entityName).join(", ") || "<none>";
|
|
113
|
-
throw new Error(`${FEATURE_NAME}: no provider selected — set the 'provider' config-key to one of: ${known}. ` +
|
|
114
|
-
`Mount a mail-transport-* feature first if no plugins are registered.`);
|
|
115
|
-
}
|
|
125
|
+
if (provider.length === 0)
|
|
126
|
+
return null;
|
|
116
127
|
const usages = ctx.registry.getExtensionUsages(MAIL_TRANSPORT_EXTENSION);
|
|
117
128
|
const usage = usages.find((u) => u.entityName === provider);
|
|
118
129
|
if (!usage) {
|
|
@@ -126,3 +137,13 @@ export async function createTransportForTenant(ctx, tenantId, handlerName = "mai
|
|
|
126
137
|
}
|
|
127
138
|
return withPiiCiphertextGuard(await usage.options.build(ctx, tenantId));
|
|
128
139
|
}
|
|
140
|
+
/** The `tenantTransport` resolver for `createChannelEmailFeature`: the tenant's configured
|
|
141
|
+
* provider, or null so the channel falls back to its app transport. */
|
|
142
|
+
export function tenantMailTransport(scope) {
|
|
143
|
+
return createTransportForTenantIfConfigured({
|
|
144
|
+
config: scope.config,
|
|
145
|
+
registry: scope.registry,
|
|
146
|
+
secrets: scope.secrets,
|
|
147
|
+
_userId: createSystemUser(scope.tenantId).id,
|
|
148
|
+
}, scope.tenantId, "channel-email");
|
|
149
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createTransportForTenant, isMailTransportPlugin, MAIL_TRANSPORT_EXTENSION, type MailTransportContext, type MailTransportPlugin, mailFoundationFeature, } from "./feature.js";
|
|
1
|
+
export { createTransportForTenant, createTransportForTenantIfConfigured, isMailTransportPlugin, MAIL_TRANSPORT_EXTENSION, type MailTransportContext, type MailTransportPlugin, mailFoundationFeature, type TenantMailTransportScope, tenantMailTransport, } from "./feature.js";
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
// up the registered transport-plugin, returns its EmailTransport
|
|
7
7
|
// - `MailTransportPlugin` — type that provider-features implement
|
|
8
8
|
// when registering via `r.useExtension("mailTransport", ...)`
|
|
9
|
-
export { createTransportForTenant, isMailTransportPlugin, MAIL_TRANSPORT_EXTENSION, mailFoundationFeature, } from "./feature.js";
|
|
9
|
+
export { createTransportForTenant, createTransportForTenantIfConfigured, isMailTransportPlugin, MAIL_TRANSPORT_EXTENSION, mailFoundationFeature, tenantMailTransport, } from "./feature.js";
|
|
@@ -62,11 +62,11 @@ export function clearInbox(tenantId) {
|
|
|
62
62
|
// =============================================================================
|
|
63
63
|
export const mailTransportInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
64
64
|
r.describe('Registers an in-process `"inmemory"` provider for `mail-foundation` that buffers sent mails per tenant instead of contacting an SMTP server. Use `getInbox(tenantId)` and `clearInbox(tenantId)` in demo apps and tests; not for production (buffer is process-memory, lost on restart).');
|
|
65
|
-
// Kein
|
|
65
|
+
// Kein requires auf "config" oder "secrets" — der
|
|
66
66
|
// In-Memory-Transport hat keine Config (nichts zu konfigurieren) und
|
|
67
67
|
// kein Secret. Der einzige hard-require ist mail-foundation, das den
|
|
68
68
|
// extension-point "mailTransport" definiert.
|
|
69
|
-
r.requires("mail-foundation");
|
|
69
|
+
r.requires({ features: ["mail-foundation"] });
|
|
70
70
|
const plugin = {
|
|
71
71
|
build: async (_ctx, tenantId) => {
|
|
72
72
|
// Returnt den per-tenant Buffer. Identitätsstabil zwischen calls
|
|
@@ -57,9 +57,9 @@ export const mailTransportSmtpFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
57
57
|
category: "notifications",
|
|
58
58
|
recommended: false,
|
|
59
59
|
});
|
|
60
|
-
r.requires("config");
|
|
61
|
-
r.requires("secrets");
|
|
62
|
-
r.requires("mail-foundation");
|
|
60
|
+
r.requires({ features: ["config"] });
|
|
61
|
+
r.requires({ features: ["secrets"] });
|
|
62
|
+
r.requires({ features: ["mail-foundation"] });
|
|
63
63
|
r.envSchema(mailTransportSmtpEnvSchema);
|
|
64
64
|
// Provider-secret. Sensitive: redact-helper for admin-UI display.
|
|
65
65
|
const password = r.secret("smtp.password", {
|
|
@@ -15,7 +15,7 @@ import { MANAGED_PAGES_CSS_FEATURE } from "./branding.js";
|
|
|
15
15
|
export function createManagedPagesCssFeature() {
|
|
16
16
|
return defineFeature(MANAGED_PAGES_CSS_FEATURE, (r) => {
|
|
17
17
|
r.describe('Per-tenant toggle gate for the managed-pages custom-CSS (raw tenant CSS-inject) capability. Handler-less: composing it makes `managed-pages-css` a toggleable feature defaulting OFF, so an operator/tier can grant raw CSS-inject per tenant. managed-pages reads `ctx.hasFeature("managed-pages-css")` in its branding query and emits tenant CSS only when this toggle is on AND the app passed `allowCustomCss: true`. The tenant CSS is allowlist-sanitized + scoped at render regardless; this gate is the commercial/operator control, not the safety boundary.');
|
|
18
|
-
r.requires("managed-pages");
|
|
18
|
+
r.requires({ features: ["managed-pages"] });
|
|
19
19
|
r.toggleable({ default: false });
|
|
20
20
|
});
|
|
21
21
|
}
|
|
@@ -86,8 +86,8 @@ export function createManagedPagesFeature(opts) {
|
|
|
86
86
|
category: "content",
|
|
87
87
|
recommended: false,
|
|
88
88
|
});
|
|
89
|
-
r.requires("config");
|
|
90
|
-
r.entity("page", pageEntity);
|
|
89
|
+
r.requires({ features: ["config"] });
|
|
90
|
+
r.entity({ name: "page", ...pageEntity });
|
|
91
91
|
// Per-tenant branding config keys (scope: tenant). Write-validated via
|
|
92
92
|
// keyDef.pattern (hex / https) — see branding.ts. read:all so the
|
|
93
93
|
// anonymous render path may resolve them. The raw-CSS key is added only
|
package/dist/metrics/feature.js
CHANGED
|
@@ -103,7 +103,7 @@ export function createMetricsFeature(options) {
|
|
|
103
103
|
recommended: false,
|
|
104
104
|
});
|
|
105
105
|
for (const feature of requiredFeatures(metrics))
|
|
106
|
-
r.requires(feature);
|
|
106
|
+
r.requires({ features: [feature] });
|
|
107
107
|
for (const metric of metrics)
|
|
108
108
|
r.queryHandler(createTenantMetricQuery(metric, now));
|
|
109
109
|
r.translations({ keys: METRICS_I18N });
|
|
@@ -122,7 +122,7 @@ export function createSystemMetricsFeature(options) {
|
|
|
122
122
|
});
|
|
123
123
|
r.systemScope();
|
|
124
124
|
for (const feature of requiredFeatures(metrics))
|
|
125
|
-
r.requires(feature);
|
|
125
|
+
r.requires({ features: [feature] });
|
|
126
126
|
for (const metric of metrics)
|
|
127
127
|
r.queryHandler(createSystemMetricQuery(metric, now));
|
|
128
128
|
r.translations({ keys: METRICS_I18N });
|
|
@@ -27,13 +27,13 @@ function registerNotesHistory(r, access, ownership, parents) {
|
|
|
27
27
|
recommended: false,
|
|
28
28
|
});
|
|
29
29
|
const entity = createNoteEntryEntity(ownership, parents);
|
|
30
|
-
r.entity("note-entry", entity);
|
|
30
|
+
r.entity({ name: "note-entry", ...entity });
|
|
31
31
|
// No write/query handler of its own — populated only as a side effect of
|
|
32
32
|
// add-note (see handlers/add-note.write.ts), looked up by
|
|
33
33
|
// notes-history-user-data's forget cascade. Registering the entity (not
|
|
34
34
|
// just building it inline in the handler) is what makes it visible to the
|
|
35
35
|
// registry-wide GDPR boot guards and to executor.ts's table/projection setup.
|
|
36
|
-
r.entity("note-mention", noteMentionEntity);
|
|
36
|
+
r.entity({ name: "note-mention", ...noteMentionEntity });
|
|
37
37
|
r.writeHandler(createAddNoteHandler(access, createNoteEntryExecutor(ownership)));
|
|
38
38
|
r.queryHandler(defineEntityListHandler("note-entry", entity, {
|
|
39
39
|
access,
|
|
@@ -13,7 +13,7 @@ export const notesHistoryUserDataFeature = defineFeature("notes-history-user-dat
|
|
|
13
13
|
// is OPTIONAL: if it's mounted toggleable(default=false), a hard r.requires
|
|
14
14
|
// would throw an "effectively disabled" boot warning even though the
|
|
15
15
|
// note-entry entity exists and the hooks work fine.
|
|
16
|
-
r.requires("user-data-rights");
|
|
16
|
+
r.requires({ features: ["user-data-rights"] });
|
|
17
17
|
r.optionalRequires("notes-history");
|
|
18
18
|
r.useExtension(EXT_USER_DATA, "note-entry", {
|
|
19
19
|
export: noteEntryExportHook,
|
|
@@ -52,7 +52,7 @@ export function createPersonalAccessTokensFeature(options) {
|
|
|
52
52
|
// roles — make both boot-time deps so a mis-wiring fails validateBoot.
|
|
53
53
|
// auth-foundation owns EXT_TOKEN_VERIFIER, which the useExtension below
|
|
54
54
|
// registers against.
|
|
55
|
-
r.requires("user", "tenant", "auth-foundation");
|
|
55
|
+
r.requires({ features: ["user", "tenant", "auth-foundation"] });
|
|
56
56
|
// Self-registers as a bearer-auth provider instead of the app wiring a
|
|
57
57
|
// dedicated patResolver callback — the middleware finds it generically
|
|
58
58
|
// via resolveTokenVerifier(), shape-matched by the PAT token prefix.
|
|
@@ -83,18 +83,22 @@ export function createPersonalAccessTokensFeature(options) {
|
|
|
83
83
|
// #497), so a postSave hook on "user" gets a poisoned ctx.db here. The
|
|
84
84
|
// concrete revoker is bound once run{Prod,Dev}App has a real db handle.
|
|
85
85
|
let autoRevokeOnPasswordChange = options.autoRevokeOnPasswordChange;
|
|
86
|
-
r.hook(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
r.hook({
|
|
87
|
+
type: "postSave",
|
|
88
|
+
target: { allOf: "user" },
|
|
89
|
+
handler: async (result) => {
|
|
90
|
+
// skip: nothing bound — same late-bind pattern as sessions/feature.ts
|
|
91
|
+
if (!autoRevokeOnPasswordChange)
|
|
92
|
+
return;
|
|
93
|
+
// skip: brand-new user, no PATs can exist yet
|
|
94
|
+
if (result.isNew)
|
|
95
|
+
return;
|
|
96
|
+
// skip: handler didn't touch any revoke-triggering field
|
|
97
|
+
if (!PAT_REVOKE_TRIGGERING_FIELDS.some((field) => result.changes[field] !== undefined)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await autoRevokeOnPasswordChange(String(result.id));
|
|
101
|
+
},
|
|
98
102
|
});
|
|
99
103
|
const bindAutoRevokeOnPasswordChange = (revoker) => {
|
|
100
104
|
// explicit constructor option wins over the runtime binding
|
|
@@ -16,8 +16,8 @@ export const readinessFeature = defineFeature("readiness", (r) => {
|
|
|
16
16
|
category: "operations",
|
|
17
17
|
recommended: false,
|
|
18
18
|
});
|
|
19
|
-
r.requires("config");
|
|
20
|
-
r.requires("secrets");
|
|
19
|
+
r.requires({ features: ["config"] });
|
|
20
|
+
r.requires({ features: ["secrets"] });
|
|
21
21
|
const queries = {
|
|
22
22
|
status: r.queryHandler(statusQuery),
|
|
23
23
|
};
|
|
@@ -17,7 +17,7 @@ export function createRendererFoundationFeature() {
|
|
|
17
17
|
category: "notifications",
|
|
18
18
|
recommended: false,
|
|
19
19
|
});
|
|
20
|
-
r.requires("template-resolver");
|
|
20
|
+
r.requires({ features: ["template-resolver"] });
|
|
21
21
|
r.extendsRegistrar(RENDERER_EXTENSION, {
|
|
22
22
|
onRegister: () => {
|
|
23
23
|
// Plugin-Konformitäts-Check könnte hier: shape-validation der
|
|
@@ -29,7 +29,7 @@ export function createRendererPdfFeature() {
|
|
|
29
29
|
category: "notifications",
|
|
30
30
|
recommended: false,
|
|
31
31
|
});
|
|
32
|
-
r.requires("renderer-foundation");
|
|
32
|
+
r.requires({ features: ["renderer-foundation"] });
|
|
33
33
|
r.useExtension(RENDERER_EXTENSION, "pdf", {
|
|
34
34
|
kinds: ["document-pdf"],
|
|
35
35
|
render: (req) => renderTablePdfRequest(req),
|
package/dist/secrets/feature.js
CHANGED
|
@@ -116,7 +116,7 @@ export function createSecretsFeature(opts = {}) {
|
|
|
116
116
|
// uniqueIndex are not expressible via the field-DSL (jsonb-without-default,
|
|
117
117
|
// now()-default), so the physical table is the DDL truth. Without this the
|
|
118
118
|
// generated migration omitted those columns → prod-500 (publicstatus#116).
|
|
119
|
-
r.entity("tenant-secret", tenantSecretEntity,
|
|
119
|
+
r.entity({ name: "tenant-secret", ...tenantSecretEntity, backingTable: tenantSecretsTable });
|
|
120
120
|
// Read-audit domain-event. Registered here so ops tools + MSPs can
|
|
121
121
|
// discover the type; secrets-context.get parses payloads against
|
|
122
122
|
// `secretReadSchema` at write time because the low-level append() path
|
package/dist/seo/feature.js
CHANGED
|
@@ -108,7 +108,7 @@ export function createSeoFeature(opts) {
|
|
|
108
108
|
// config keys need the config feature's write/read machinery to actually
|
|
109
109
|
// function (same hard dependency managed-pages has) — only legal-pages/
|
|
110
110
|
// managed-pages are genuinely optional (merged only when opted in).
|
|
111
|
-
r.requires("config");
|
|
111
|
+
r.requires({ features: ["config"] });
|
|
112
112
|
r.optionalRequires("legal-pages", "managed-pages");
|
|
113
113
|
r.config({ keys: SEO_CONFIG_KEYS });
|
|
114
114
|
r.queryHandler(seoConfigQuery);
|
package/dist/sessions/feature.js
CHANGED
|
@@ -59,7 +59,7 @@ export function createSessionsFeature(options) {
|
|
|
59
59
|
// sessions-without-user wiring fails validateBoot instead of 500ing live.
|
|
60
60
|
// auth-foundation owns EXT_SESSION_STORE, which the useExtension below
|
|
61
61
|
// registers against.
|
|
62
|
-
r.requires("user", "auth-foundation");
|
|
62
|
+
r.requires({ features: ["user", "auth-foundation"] });
|
|
63
63
|
// store_user_sessions is a hot-path direct-write store: sessionCreator
|
|
64
64
|
// inserts and the revoke handlers update rows WITHOUT emitting lifecycle
|
|
65
65
|
// events (the row columns ARE the audit trail). Registering it as
|
|
@@ -144,21 +144,25 @@ export function createSessionsFeature(options) {
|
|
|
144
144
|
// regardless.
|
|
145
145
|
const REVOKE_TRIGGERING_FIELDS = ["passwordHash"];
|
|
146
146
|
let autoRevoke = options?.autoRevokeOnPasswordChange;
|
|
147
|
-
r.hook(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
147
|
+
r.hook({
|
|
148
|
+
type: "postSave",
|
|
149
|
+
target: { allOf: "user" },
|
|
150
|
+
handler: async (ctx) => {
|
|
151
|
+
// skip: nothing bound — stateless-JWT deployments without a runtime
|
|
152
|
+
// that calls bindAutoRevokeOnPasswordChange keep the old behavior.
|
|
153
|
+
if (!autoRevoke)
|
|
154
|
+
return;
|
|
155
|
+
// skip: brand-new user, no sessions can possibly exist yet. The
|
|
156
|
+
// initial passwordHash on a user:create would trip the second guard
|
|
157
|
+
// otherwise — every registration would do a mass-revoke roundtrip
|
|
158
|
+
// for a user who literally has no rows in user_sessions.
|
|
159
|
+
if (ctx.isNew)
|
|
160
|
+
return;
|
|
161
|
+
// skip: handler didn't touch any revoke-triggering field
|
|
162
|
+
if (!REVOKE_TRIGGERING_FIELDS.some((field) => ctx.changes[field] !== undefined))
|
|
163
|
+
return;
|
|
164
|
+
await autoRevoke(String(ctx.id));
|
|
165
|
+
},
|
|
162
166
|
});
|
|
163
167
|
const bindAutoRevokeOnPasswordChange = (revoker) => {
|
|
164
168
|
// explicit constructor option wins over the runtime binding
|
|
@@ -34,7 +34,7 @@ export function createStepDispatcherFeature() {
|
|
|
34
34
|
recommended: false,
|
|
35
35
|
});
|
|
36
36
|
r.envSchema(stepDispatcherEnvSchema);
|
|
37
|
-
r.requires("secrets");
|
|
37
|
+
r.requires({ features: ["secrets"] });
|
|
38
38
|
// Literal name: the few-shot corpus parser only reads string-literal job names.
|
|
39
39
|
// StepDispatcherJobNames.deliver mirrors it; feature.boot.test.ts pins the two together.
|
|
40
40
|
r.job("deliver", {
|
|
@@ -102,7 +102,7 @@ export function createSubscriptionMollieFeature(options) {
|
|
|
102
102
|
category: "billing",
|
|
103
103
|
recommended: false,
|
|
104
104
|
});
|
|
105
|
-
r.requires("billing-foundation");
|
|
105
|
+
r.requires({ features: ["billing-foundation"] });
|
|
106
106
|
r.envSchema(subscriptionMollieEnvSchema);
|
|
107
107
|
const plugin = {
|
|
108
108
|
verifyAndParseWebhook: verifyAndParse,
|
|
@@ -93,9 +93,9 @@ export function createSubscriptionStripeFeature(options = {}) {
|
|
|
93
93
|
// Hard-deps: billing-foundation (plugin-host) + config (billing-live +
|
|
94
94
|
// backing:"secrets" credentials) + secrets (the store the backing:secrets
|
|
95
95
|
// dispatch reads/writes + its tenant_secrets table).
|
|
96
|
-
r.requires("billing-foundation");
|
|
97
|
-
r.requires("config");
|
|
98
|
-
r.requires("secrets");
|
|
96
|
+
r.requires({ features: ["billing-foundation"] });
|
|
97
|
+
r.requires({ features: ["config"] });
|
|
98
|
+
r.requires({ features: ["secrets"] });
|
|
99
99
|
r.envSchema(subscriptionStripeEnvSchema);
|
|
100
100
|
// Runtime config. api-key + webhook-secret declare backing:"secrets" —
|
|
101
101
|
// the value lives envelope-encrypted in the secrets store under
|
package/dist/tags/feature.js
CHANGED
|
@@ -41,8 +41,8 @@ function registerTags(r, access, toggleable, ownership, parents) {
|
|
|
41
41
|
if (toggleable !== undefined)
|
|
42
42
|
r.toggleable(toggleable);
|
|
43
43
|
const tagAssignmentEntity = createTagAssignmentEntity(ownership, parents);
|
|
44
|
-
r.entity("tag", tagEntity);
|
|
45
|
-
r.entity("tag-assignment", tagAssignmentEntity);
|
|
44
|
+
r.entity({ name: "tag", ...tagEntity });
|
|
45
|
+
r.entity({ name: "tag-assignment", ...tagAssignmentEntity });
|
|
46
46
|
r.writeHandler(createCreateTagHandler(access));
|
|
47
47
|
r.writeHandler(createUpdateTagHandler(access));
|
|
48
48
|
r.writeHandler(createDeleteTagHandler(access));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
2
|
import { isResourceImageContentType } from "../shared/index.js";
|
|
3
3
|
import { rewriteStepsBlock } from "./compile-steps-block.js";
|
|
4
|
-
import { withParsedDocument } from "./document-sanitizer.js";
|
|
5
|
-
import { DOCUMENT_TEXT_ALIGNMENTS, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
4
|
+
import { conditionScopeElement, withParsedDocument } from "./document-sanitizer.js";
|
|
5
|
+
import { DOCUMENT_CONDITION_ATTRIBUTE, DOCUMENT_TEXT_ALIGNMENTS, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
6
6
|
// Private-use characters mark where a Handlebars expression goes. Author text
|
|
7
7
|
// is scrubbed of them first and every `{`/`}` is entity-encoded before the
|
|
8
8
|
// markers are swapped for real expressions, so the only Handlebars syntax in
|
|
@@ -34,19 +34,24 @@ function resolveReference(name, scope, variables) {
|
|
|
34
34
|
}
|
|
35
35
|
return { path: scope === null ? name : `@root.${name}`, def: ownVariable(variables, name) };
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
// The sanitized rich text value always consists of blocks; inside a line of
|
|
38
|
+
// other text it renders inline so no paragraph ends up inside a paragraph.
|
|
39
|
+
function valueExpression(path, def, isWholeBlock) {
|
|
38
40
|
if (def?.type === "money")
|
|
39
41
|
return `{{money ${path}}}`;
|
|
40
42
|
if (def?.type === "date")
|
|
41
43
|
return `{{date ${path}}}`;
|
|
42
|
-
if (def?.type === "
|
|
43
|
-
return `{{
|
|
44
|
+
if (def?.type === "percent")
|
|
45
|
+
return `{{percent ${path}}}`;
|
|
46
|
+
if (def?.type === "richText") {
|
|
47
|
+
return isWholeBlock ? `{{richText ${path}}}` : `{{richText ${path} inline=true}}`;
|
|
48
|
+
}
|
|
44
49
|
return `{{${path}}}`;
|
|
45
50
|
}
|
|
46
51
|
function rewriteVariable(element, state) {
|
|
47
52
|
const name = element.getAttribute("data-variable") ?? "";
|
|
48
53
|
const replacement = element.ownerDocument.createTextNode(isDocumentReferenceName(name)
|
|
49
|
-
? marker(state, valueExpression(...pathAndDef(name, element, state)))
|
|
54
|
+
? marker(state, valueExpression(...pathAndDef(name, element, state), state.blockRichTextSpans.has(element)))
|
|
50
55
|
: "");
|
|
51
56
|
element.replaceWith(replacement);
|
|
52
57
|
}
|
|
@@ -76,10 +81,25 @@ function wrapVariableOnlyBlock(block, state) {
|
|
|
76
81
|
// skip: a line with static content or several placeholders stays as it is
|
|
77
82
|
if (name === null)
|
|
78
83
|
return;
|
|
79
|
-
const [path] = pathAndDef(name, block, state);
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
const [path, def] = pathAndDef(name, block, state);
|
|
85
|
+
const host = def?.type === "richText" && block.tagName === "P" ? richTextBlockHost(block, state) : block;
|
|
86
|
+
const document = host.ownerDocument;
|
|
87
|
+
host.before(document.createTextNode(marker(state, `{{#if ${path} includeZero=true}}`)));
|
|
88
|
+
host.after(document.createTextNode(marker(state, "{{/if}}")));
|
|
89
|
+
}
|
|
90
|
+
// A paragraph cannot hold the paragraphs of a rich text value, a div can; it
|
|
91
|
+
// keeps the paragraph's attributes so alignment and size still apply.
|
|
92
|
+
function richTextBlockHost(paragraph, state) {
|
|
93
|
+
const host = paragraph.ownerDocument.createElement("div");
|
|
94
|
+
for (const attribute of Array.from(paragraph.attributes)) {
|
|
95
|
+
host.setAttribute(attribute.name, attribute.value ?? "");
|
|
96
|
+
}
|
|
97
|
+
for (const span of Array.from(paragraph.querySelectorAll("span[data-variable]"))) {
|
|
98
|
+
state.blockRichTextSpans.add(span);
|
|
99
|
+
}
|
|
100
|
+
host.append(...Array.from(paragraph.childNodes));
|
|
101
|
+
paragraph.replaceWith(host);
|
|
102
|
+
return host;
|
|
83
103
|
}
|
|
84
104
|
function rewriteLink(element, state) {
|
|
85
105
|
const href = element.getAttribute("href") ?? "";
|
|
@@ -110,6 +130,19 @@ function rewriteRepeatRow(row, state) {
|
|
|
110
130
|
row.removeAttribute("data-repeat-header");
|
|
111
131
|
}
|
|
112
132
|
}
|
|
133
|
+
function rewriteConditionalBlock(block, state) {
|
|
134
|
+
const name = block.getAttribute(DOCUMENT_CONDITION_ATTRIBUTE) ?? "";
|
|
135
|
+
block.removeAttribute(DOCUMENT_CONDITION_ATTRIBUTE);
|
|
136
|
+
if (!isDocumentReferenceName(name)) {
|
|
137
|
+
block.remove();
|
|
138
|
+
// skip: a condition on a name that cannot be a variable never shows
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const [path] = pathAndDef(name, conditionScopeElement(block), state);
|
|
142
|
+
const document = block.ownerDocument;
|
|
143
|
+
block.before(document.createTextNode(marker(state, `{{#if ${path} includeZero=true}}`)));
|
|
144
|
+
block.after(document.createTextNode(marker(state, "{{/if}}")));
|
|
145
|
+
}
|
|
113
146
|
function stripActiveContent(element) {
|
|
114
147
|
for (const attribute of Array.from(element.attributes)) {
|
|
115
148
|
if (/^on/i.test(attribute.name))
|
|
@@ -223,7 +256,7 @@ function rewriteLinks(root, state, options) {
|
|
|
223
256
|
// rewritten in document order; list rows last so their placeholders still see
|
|
224
257
|
// the `tr[data-each]` ancestor.
|
|
225
258
|
export function compileDocumentBodyToHandlebars(html, variables, options = {}) {
|
|
226
|
-
const state = { variables, expressions: [] };
|
|
259
|
+
const state = { variables, expressions: [], blockRichTextSpans: new Set() };
|
|
227
260
|
const scrubbed = html.replaceAll(EXPRESSION_OPEN, "").replaceAll(EXPRESSION_CLOSE, "");
|
|
228
261
|
const compiled = withParsedDocument(scrubbed, (root) => {
|
|
229
262
|
stripScriptsAndActiveContent(root, options);
|
|
@@ -240,6 +273,9 @@ export function compileDocumentBodyToHandlebars(html, variables, options = {}) {
|
|
|
240
273
|
color: options.stepsColor,
|
|
241
274
|
});
|
|
242
275
|
}
|
|
276
|
+
for (const block of Array.from(root.querySelectorAll(`[${DOCUMENT_CONDITION_ATTRIBUTE}]`))) {
|
|
277
|
+
rewriteConditionalBlock(block, state);
|
|
278
|
+
}
|
|
243
279
|
for (const block of Array.from(root.querySelectorAll(VARIABLE_ONLY_BLOCK_SELECTOR))) {
|
|
244
280
|
wrapVariableOnlyBlock(block, state);
|
|
245
281
|
}
|
|
@@ -24,9 +24,11 @@ export type SanitizeBodyResult = {
|
|
|
24
24
|
export declare function violation(code: DocumentViolationCode, detail: string, message: string): DocumentViolation;
|
|
25
25
|
export declare function withParsedDocument<T>(html: string, read: (body: ScanElement) => T): T;
|
|
26
26
|
export declare function sanitizeDocumentHtml(html: string): SanitizeHtmlResult;
|
|
27
|
+
export declare function flattenParagraphsToLineBreaks(sanitizedHtml: string): string;
|
|
27
28
|
export declare function sanitizeTemplateBody(input: {
|
|
28
29
|
readonly kind: string;
|
|
29
30
|
readonly contentFormat: ContentFormat;
|
|
30
31
|
readonly content: string | null;
|
|
31
32
|
}): SanitizeBodyResult;
|
|
32
33
|
export declare function documentViolationsToFieldIssues(violations: readonly DocumentViolation[]): FieldIssue[];
|
|
34
|
+
export declare function conditionScopeElement(block: ScanElement): ScanElement;
|
|
@@ -2,7 +2,7 @@ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
|
2
2
|
import { generateHTML, generateJSON } from "@tiptap/html/server";
|
|
3
3
|
import { Window } from "happy-dom";
|
|
4
4
|
import { DOCUMENT_HTML_KINDS } from "./constants.js";
|
|
5
|
-
import { documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
5
|
+
import { DOCUMENT_CONDITION_ATTRIBUTE, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
6
6
|
import { isDocumentStepKey } from "./document-steps.js";
|
|
7
7
|
export function violation(code, detail, message) {
|
|
8
8
|
return { code, detail, message };
|
|
@@ -56,6 +56,7 @@ function scanImage(element, out) {
|
|
|
56
56
|
const REFERENCE_ATTRIBUTES = [
|
|
57
57
|
["data-variable", isDocumentReferenceName],
|
|
58
58
|
["data-each", isDocumentReferenceName],
|
|
59
|
+
[DOCUMENT_CONDITION_ATTRIBUTE, isDocumentReferenceName],
|
|
59
60
|
["data-resource", isDocumentResourceKey],
|
|
60
61
|
["data-steps", isDocumentReferenceName],
|
|
61
62
|
["data-step", isDocumentStepKey],
|
|
@@ -155,6 +156,15 @@ export function sanitizeDocumentHtml(html) {
|
|
|
155
156
|
html: generateHTML(generateJSON(html, documentExtensions), documentExtensions),
|
|
156
157
|
};
|
|
157
158
|
}
|
|
159
|
+
// For a sanitized value placed inside a line of text: paragraphs become line
|
|
160
|
+
// breaks. Other blocks (lists, headings) cannot be made inline and stay as they are.
|
|
161
|
+
export function flattenParagraphsToLineBreaks(sanitizedHtml) {
|
|
162
|
+
return withParsedDocument(sanitizedHtml, (body) => {
|
|
163
|
+
const blocks = Array.from(body.children);
|
|
164
|
+
const isParagraphsOnly = blocks.length === body.childNodes.length && blocks.every((block) => block.tagName === "P");
|
|
165
|
+
return isParagraphsOnly ? blocks.map((block) => block.innerHTML).join("<br>") : sanitizedHtml;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
158
168
|
// Snippets accept any format, but their html is as untrusted as a mail body.
|
|
159
169
|
function isSanitizedHtmlKind(kind) {
|
|
160
170
|
return kind === "snippet" || isOneOf(DOCUMENT_HTML_KINDS, kind);
|
|
@@ -202,3 +212,8 @@ export function documentViolationsToFieldIssues(violations) {
|
|
|
202
212
|
params: { detail: item.detail },
|
|
203
213
|
}));
|
|
204
214
|
}
|
|
215
|
+
// A repeat row's own data-if is decided per list, outside the row's item scope,
|
|
216
|
+
// because the compiled {{#if}} lands outside its {{#each}}.
|
|
217
|
+
export function conditionScopeElement(block) {
|
|
218
|
+
return block.matches("tr[data-each]") ? (block.parentElement ?? block) : block;
|
|
219
|
+
}
|
|
@@ -19,4 +19,6 @@ export declare const DOCUMENT_TEXT_ALIGNMENTS: readonly ["left", "center", "righ
|
|
|
19
19
|
export type DocumentTextAlignment = (typeof DOCUMENT_TEXT_ALIGNMENTS)[number];
|
|
20
20
|
export declare const DocumentTextAlign: Extension<any, any>;
|
|
21
21
|
export declare const DocumentTextSize: Extension<any, any>;
|
|
22
|
+
export declare const DOCUMENT_CONDITION_ATTRIBUTE = "data-if";
|
|
23
|
+
export declare const DocumentCondition: Extension<any, any>;
|
|
22
24
|
export declare const documentExtensions: Extensions;
|