@cosmicdrift/kumiko-samples 0.393.0 → 0.395.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +15 -5
- package/packages/bundled-features/src/account-security/README.md +1 -1
- package/packages/bundled-features/src/account-security/changes.json +6 -0
- package/packages/bundled-features/src/account-security/feature.ts +9 -1
- package/packages/bundled-features/src/audit/event-classification.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
- package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
- package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
- package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
- package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
- package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
- package/packages/bundled-features/src/cap-overview/types.ts +2 -0
- package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
- package/packages/bundled-features/src/files/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +143 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
- package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
- package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
- package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
- package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -2
- package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +93 -15
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
- package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
- package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
- package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
- package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +238 -89
- package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
- package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
- package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
- package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
- package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
- package/packages/bundled-features/src/inbound-provider-imap/changes.json +36 -0
- package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +124 -41
- package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/constants.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +61 -136
- package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
- package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +9 -0
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
- package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
- package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
- package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +4 -6
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +75 -45
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +19 -8
- package/packages/bundled-features/src/inbound-provider-imap/screens.ts +203 -109
- package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
- package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
- package/packages/bundled-features/src/inbound-provider-imap/watch-mailbox.ts +109 -0
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
- package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
- package/packages/bundled-features/src/jobs/changes.json +18 -0
- package/packages/bundled-features/src/managed-pages/changes.json +7 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +7 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +6 -4
- package/packages/bundled-features/src/managed-pages/handlers/by-slug.query.ts +2 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-body-guard.ts +61 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-body-issues.ts +26 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-crud.ts +17 -10
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +15 -4
- package/packages/bundled-features/src/managed-pages/render-page-body.ts +28 -0
- package/packages/bundled-features/src/managed-pages/screens/page-screens.ts +6 -2
- package/packages/bundled-features/src/managed-pages/seeding.ts +13 -2
- package/packages/bundled-features/src/managed-pages/table.ts +6 -2
- package/packages/bundled-features/src/managed-pages/web/client-plugin.tsx +6 -4
- package/packages/bundled-features/src/managed-pages/web/constants.ts +14 -0
- package/packages/bundled-features/src/managed-pages/web/i18n.ts +14 -0
- package/packages/bundled-features/src/managed-pages/web/lossy-markdown.ts +71 -0
- package/packages/bundled-features/src/managed-pages/web/markdown-to-document.ts +27 -0
- package/packages/bundled-features/src/managed-pages/web/page-body-section.tsx +73 -0
- package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
- package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
- package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
- package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
- package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
- package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
- package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
- package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
- package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
- package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
- package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
- package/packages/bundled-features/src/template-resolver/README.md +2 -2
- package/packages/bundled-features/src/template-resolver/changes.json +20 -0
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +16 -16
- package/packages/bundled-features/src/template-resolver/feature.ts +14 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +24 -6
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +32 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +1 -0
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +9 -3
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +13 -480
- package/packages/bundled-features/src/template-resolver/web/collection-entry-create-form.tsx +105 -0
- package/packages/bundled-features/src/template-resolver/web/collection-lookup.ts +33 -0
- package/packages/bundled-features/src/template-resolver/web/constants.ts +3 -0
- package/packages/bundled-features/src/template-resolver/web/content-field.tsx +61 -0
- package/packages/bundled-features/src/template-resolver/web/content-tree-provider.ts +256 -0
- package/packages/bundled-features/src/template-resolver/web/document-reference-name.ts +12 -0
- package/packages/bundled-features/src/template-resolver/web/document-schema.ts +3 -11
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +9 -2
- package/packages/bundled-features/src/template-resolver/web/legacy-variable-text.ts +30 -0
- package/packages/bundled-features/src/template-resolver/web/tenant-override.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/text-block-editor.tsx +92 -0
- package/packages/bundled-features/src/template-resolver/web/use-collection-editor-context.ts +42 -0
- package/packages/bundled-features/src/template-resolver/web/use-collection-entry-create.ts +92 -0
- package/packages/bundled-features/src/template-resolver/web/use-content-write.ts +81 -0
- package/packages/bundled-features/src/template-resolver/web/use-text-block-editor.ts +127 -0
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +9 -1
- package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +89 -40
- package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
- package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
- package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
- package/samples/apps/use-all-bundled/src/app/client.tsx +8 -1
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
- package/samples/apps/use-all-bundled/src/app/seed.ts +17 -2
- package/samples/apps/use-all-bundled/src/run-config.ts +19 -11
- package/samples/recipes/cross-feature-events/README.md +10 -0
- package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
- package/samples/recipes/dashboard-stats/README.md +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
- package/samples/recipes/env-composition/README.md +2 -2
- package/samples/recipes/env-composition/src/env.ts +2 -2
- package/samples/recipes/inbound-mail-triage/README.md +8 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
- package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
- package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
- package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
- package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
- package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { dispatcherErrorText, useDispatcher, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { DispatcherError } from "@cosmicdrift/kumiko-renderer/headless";
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { isTenantOverrideDeniedError } from "./tenant-override.ts";
|
|
5
|
+
|
|
6
|
+
export type TenantScopedWrite = {
|
|
7
|
+
readonly response: ContentWriteResponse;
|
|
8
|
+
/** The override the write was accepted with; undefined after a denied override fell back to the session tenant. */
|
|
9
|
+
readonly tenantIdOverride: string | undefined;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ContentWriteResponse = {
|
|
13
|
+
readonly slug: string;
|
|
14
|
+
readonly locale: string;
|
|
15
|
+
readonly isNew: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
19
|
+
|
|
20
|
+
// A validation failure names the offending field with its own message; prefer that over the generic one.
|
|
21
|
+
function writeErrorText(error: DispatcherError, t: Translate): string {
|
|
22
|
+
const issue = error.details?.fields?.[0];
|
|
23
|
+
if (issue !== undefined) {
|
|
24
|
+
const translated = t(issue.i18nKey, issue.params);
|
|
25
|
+
if (translated !== issue.i18nKey) return translated;
|
|
26
|
+
}
|
|
27
|
+
return dispatcherErrorText(error, t) || t("template-resolver.editor.saveFailed");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Dispatches a content set and keeps the submit state; `write` resolves to the response,
|
|
31
|
+
// or null after the failure text has been set in `error`.
|
|
32
|
+
export function useContentWrite() {
|
|
33
|
+
const dispatcher = useDispatcher();
|
|
34
|
+
const t = useTranslation();
|
|
35
|
+
const [submitting, setSubmitting] = useState(false);
|
|
36
|
+
const [error, setError] = useState<string | null>(null);
|
|
37
|
+
|
|
38
|
+
const writeScoped = async (
|
|
39
|
+
handler: string,
|
|
40
|
+
payload: Readonly<Record<string, unknown>>,
|
|
41
|
+
requestedOverride: string | undefined,
|
|
42
|
+
): Promise<TenantScopedWrite | null> => {
|
|
43
|
+
setSubmitting(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
try {
|
|
46
|
+
let tenantIdOverride = requestedOverride;
|
|
47
|
+
let result = await dispatcher.write<ContentWriteResponse>(handler, {
|
|
48
|
+
...payload,
|
|
49
|
+
...(tenantIdOverride !== undefined && { tenantIdOverride }),
|
|
50
|
+
});
|
|
51
|
+
if (
|
|
52
|
+
!result.isSuccess &&
|
|
53
|
+
tenantIdOverride !== undefined &&
|
|
54
|
+
isTenantOverrideDeniedError(result.error)
|
|
55
|
+
) {
|
|
56
|
+
tenantIdOverride = undefined;
|
|
57
|
+
result = await dispatcher.write<ContentWriteResponse>(handler, payload);
|
|
58
|
+
}
|
|
59
|
+
if (result.isSuccess) return { response: result.data, tenantIdOverride };
|
|
60
|
+
setError(writeErrorText(result.error, t));
|
|
61
|
+
return null;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// Network blip / dispatcher throw — otherwise submitting stays true and
|
|
64
|
+
// the save button is locked forever with no feedback.
|
|
65
|
+
setError(e instanceof Error ? e.message : t("template-resolver.editor.networkError"));
|
|
66
|
+
return null;
|
|
67
|
+
} finally {
|
|
68
|
+
setSubmitting(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const write = async (
|
|
73
|
+
handler: string,
|
|
74
|
+
payload: Readonly<Record<string, unknown>>,
|
|
75
|
+
): Promise<ContentWriteResponse | null> =>
|
|
76
|
+
(await writeScoped(handler, payload, undefined))?.response ?? null;
|
|
77
|
+
|
|
78
|
+
const clearError = useCallback(() => setError(null), []);
|
|
79
|
+
|
|
80
|
+
return { submitting, error, clearError, write, writeScoped };
|
|
81
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { TargetRef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { refreshTree, TEXT_BLOCK_TREE_KEY } from "./content-tree-provider.ts";
|
|
5
|
+
import { convertLegacyVariableText } from "./legacy-variable-text.ts";
|
|
6
|
+
import {
|
|
7
|
+
collectionHandlerName,
|
|
8
|
+
collectionQueryName,
|
|
9
|
+
TemplateResolverHandlers,
|
|
10
|
+
TemplateResolverQueries,
|
|
11
|
+
} from "./qualified-names.ts";
|
|
12
|
+
import { useCollectionEditorContext } from "./use-collection-editor-context.ts";
|
|
13
|
+
import { useContentWrite } from "./use-content-write.ts";
|
|
14
|
+
|
|
15
|
+
type TextBlock = {
|
|
16
|
+
readonly slug: string;
|
|
17
|
+
readonly locale: string;
|
|
18
|
+
readonly title: string | null;
|
|
19
|
+
readonly content: string | null;
|
|
20
|
+
readonly contentFormat: string;
|
|
21
|
+
readonly folder: string | null;
|
|
22
|
+
readonly modifiedAt: string | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type EditTargetArgs = {
|
|
26
|
+
slug?: string;
|
|
27
|
+
locale?: string;
|
|
28
|
+
collectionId?: string;
|
|
29
|
+
tenantIdOverride?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function useTextBlockEditor(target: TargetRef) {
|
|
33
|
+
// @cast-boundary visual-tree-args — TargetRef.args is erased to
|
|
34
|
+
// Record<string, unknown>; the resolver knows the action shape (see the
|
|
35
|
+
// treeActions.edit definition in feature.ts) and de-erases per action, the
|
|
36
|
+
// same way event payloads do. Optional chaining absorbs missing fields so a
|
|
37
|
+
// hand-edited URL cannot crash the editor.
|
|
38
|
+
const args = target.args as EditTargetArgs | undefined;
|
|
39
|
+
const slug = args?.slug ?? "";
|
|
40
|
+
const locale = args?.locale ?? "";
|
|
41
|
+
const tenantIdOverride = args?.tenantIdOverride;
|
|
42
|
+
// Which collection the node came from. Absent → the public text-block pair,
|
|
43
|
+
// which is what the hand-wired content tree uses.
|
|
44
|
+
const collectionId = args?.collectionId;
|
|
45
|
+
|
|
46
|
+
const t = useTranslation();
|
|
47
|
+
const editorContext = useCollectionEditorContext(collectionId);
|
|
48
|
+
const { contentFormat, canWrite } = editorContext;
|
|
49
|
+
const { submitting, error: saveError, clearError, write } = useContentWrite();
|
|
50
|
+
|
|
51
|
+
// Same split as the tree provider: a node without a collection reads through
|
|
52
|
+
// the public by-slug, a collection node through that collection's handlers.
|
|
53
|
+
const readQuery =
|
|
54
|
+
collectionId === undefined
|
|
55
|
+
? TemplateResolverQueries.bySlug
|
|
56
|
+
: collectionQueryName(collectionId, "item");
|
|
57
|
+
const writeHandler =
|
|
58
|
+
collectionId === undefined ? TemplateResolverHandlers.set : collectionHandlerName(collectionId);
|
|
59
|
+
const {
|
|
60
|
+
data: loaded,
|
|
61
|
+
loading,
|
|
62
|
+
error: loadError,
|
|
63
|
+
} = useQuery<TextBlock | null>(
|
|
64
|
+
readQuery,
|
|
65
|
+
{ slug, locale, ...(tenantIdOverride !== undefined && { tenantIdOverride }) },
|
|
66
|
+
{ enabled: slug !== "" && locale !== "" },
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const [title, setTitle] = useState("");
|
|
70
|
+
const [content, setContent] = useState("");
|
|
71
|
+
const [savedMsg, setSavedMsg] = useState<string | null>(null);
|
|
72
|
+
|
|
73
|
+
// Sync loaded data into the form state. loaded === null means the block does
|
|
74
|
+
// not exist yet — empty form for the create flow.
|
|
75
|
+
// kumiko-lint-ignore no-raw-hooks not yet converted to the framework hook set
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
// skip: keep the form untouched until the query settles
|
|
78
|
+
if (loading) return;
|
|
79
|
+
const loadedContent = loaded?.content ?? "";
|
|
80
|
+
setTitle(loaded?.title ?? "");
|
|
81
|
+
setContent(
|
|
82
|
+
contentFormat === "document" ? convertLegacyVariableText(loadedContent) : loadedContent,
|
|
83
|
+
);
|
|
84
|
+
clearError();
|
|
85
|
+
setSavedMsg(null);
|
|
86
|
+
}, [loading, loaded, contentFormat, clearError]);
|
|
87
|
+
|
|
88
|
+
const save = async (): Promise<void> => {
|
|
89
|
+
setSavedMsg(null);
|
|
90
|
+
const response = await write(writeHandler, {
|
|
91
|
+
slug,
|
|
92
|
+
locale,
|
|
93
|
+
title,
|
|
94
|
+
content: content.length > 0 ? content : null,
|
|
95
|
+
folder: loaded?.folder ?? null,
|
|
96
|
+
// A collection derives its storage format on the server; the public set keeps what was saved.
|
|
97
|
+
...(collectionId === undefined &&
|
|
98
|
+
loaded?.contentFormat !== undefined && { contentFormat: loaded.contentFormat }),
|
|
99
|
+
...(tenantIdOverride !== undefined && { tenantIdOverride }),
|
|
100
|
+
});
|
|
101
|
+
// skip: useContentWrite already set the failure text
|
|
102
|
+
if (response === null) return;
|
|
103
|
+
refreshTree(collectionId ?? TEXT_BLOCK_TREE_KEY);
|
|
104
|
+
setSavedMsg(
|
|
105
|
+
response.isNew ? t("template-resolver.editor.created") : t("template-resolver.editor.saved"),
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
slug,
|
|
111
|
+
locale,
|
|
112
|
+
editorContext,
|
|
113
|
+
canWrite,
|
|
114
|
+
loading,
|
|
115
|
+
loadError,
|
|
116
|
+
submitting,
|
|
117
|
+
saveError,
|
|
118
|
+
savedMsg,
|
|
119
|
+
disabled: submitting || loading || !canWrite,
|
|
120
|
+
saveLabelKey: submitting ? "template-resolver.editor.saving" : "template-resolver.editor.save",
|
|
121
|
+
title,
|
|
122
|
+
setTitle,
|
|
123
|
+
content,
|
|
124
|
+
setContent,
|
|
125
|
+
save,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.395.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "The tier resolver no longer keeps tier writes made before its build",
|
|
6
|
+
"detail": "A tier write that ran before build() cached a feature set without the always-on features, and a later build() left that entry in place, so the tenant got feature_disabled instead of the default fallback. build() now starts from an empty cache and writes before it are not cached."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.392.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -337,6 +337,7 @@ export function createTierEngineFeature<
|
|
|
337
337
|
// reload result is applied only while its ticket is still the tenant's latest.
|
|
338
338
|
let ticketCounter = 0;
|
|
339
339
|
const latestTicket = new Map<TenantId, number>();
|
|
340
|
+
let isBuilt = false;
|
|
340
341
|
const takeTicket = (tenantId: TenantId): number => {
|
|
341
342
|
ticketCounter += 1;
|
|
342
343
|
latestTicket.set(tenantId, ticketCounter);
|
|
@@ -370,7 +371,9 @@ export function createTierEngineFeature<
|
|
|
370
371
|
// bus and re-read the row.
|
|
371
372
|
const applyAssignment = (tenantId: TenantId, tier: string): void => {
|
|
372
373
|
takeTicket(tenantId);
|
|
373
|
-
|
|
374
|
+
// Before build() the always-on set is empty; a cached entry would outlive into the next build.
|
|
375
|
+
if (isBuilt)
|
|
376
|
+
cache.set(tenantId, mergeAlwaysOn(alwaysOnHolder.set, featuresForTier(tierMap, tier)));
|
|
374
377
|
publishAssignmentChange(tenantId);
|
|
375
378
|
};
|
|
376
379
|
onTierAssigned.fn = applyAssignment;
|
|
@@ -516,6 +519,11 @@ export function createTierEngineFeature<
|
|
|
516
519
|
if (feature.toggleableDefault === undefined) computedAlwaysOn.add(feature.name);
|
|
517
520
|
}
|
|
518
521
|
alwaysOnHolder.set = computedAlwaysOn;
|
|
522
|
+
// One feature instance can serve several stacks in a process; rebuild state from this DB only.
|
|
523
|
+
// Safe vs. "never clear-then-refill": no request reaches this resolver before build() returns.
|
|
524
|
+
cache.clear();
|
|
525
|
+
latestTicket.clear();
|
|
526
|
+
isBuilt = true;
|
|
519
527
|
|
|
520
528
|
// Pre-load all existing assignments into the cache. SaaS apps typically
|
|
521
529
|
// have <100k tenants, so a single-pass scan is acceptable. The scaling
|
package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts
CHANGED
|
@@ -177,7 +177,8 @@ function isPersonalFileRow(registry: Registry, row: Record<string, unknown>): bo
|
|
|
177
177
|
if (row["entityId"] === null && resolveActionFormFileField(registry, entityType, fieldName)) {
|
|
178
178
|
return true;
|
|
179
179
|
}
|
|
180
|
-
const
|
|
180
|
+
const entity = registry.getEntity(entityType) ?? registry.getReadModelEntity(entityType);
|
|
181
|
+
const fieldDef = entity?.fields[fieldName];
|
|
181
182
|
return isPersonalPiiField(fieldDef);
|
|
182
183
|
}
|
|
183
184
|
|
|
@@ -19,3 +19,7 @@ export const GALLERY_MOR_PRICE_IDS = {
|
|
|
19
19
|
starter: "price_gallery_mor_starter",
|
|
20
20
|
pro: "price_gallery_mor_pro",
|
|
21
21
|
};
|
|
22
|
+
|
|
23
|
+
// Set by the docs gallery config: net catalog prices with a "plus VAT" note and a gross recurring
|
|
24
|
+
// price in the plan-switch preview. The German review gallery runs without it and stays unchanged.
|
|
25
|
+
export const GALLERY_DOCS_VARIANT_ENV = "GALLERY_DOCS_VARIANT";
|
|
@@ -20,6 +20,7 @@ import type { ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
|
|
|
20
20
|
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
21
21
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
22
22
|
import {
|
|
23
|
+
GALLERY_DOCS_VARIANT_ENV,
|
|
23
24
|
GALLERY_MOR_PRICE_IDS,
|
|
24
25
|
GALLERY_MOR_PROVIDER,
|
|
25
26
|
GALLERY_OPERATOR_EMAIL,
|
|
@@ -29,6 +30,8 @@ import {
|
|
|
29
30
|
GALLERY_TERMS_SLUG,
|
|
30
31
|
} from "./gallery-constants";
|
|
31
32
|
|
|
33
|
+
const isDocsVariant = process.env[GALLERY_DOCS_VARIANT_ENV] === "1";
|
|
34
|
+
|
|
32
35
|
export const galleryMailTransport = createInMemoryTransport();
|
|
33
36
|
|
|
34
37
|
// The merchant-of-record mock is only offered to tenants the spec opts in, so
|
|
@@ -93,6 +96,7 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
|
|
|
93
96
|
tierLabelKey: (tier) => `gallery-screens:plan.${tier}.label`,
|
|
94
97
|
benefits: (tier) => [{ labelKey: `gallery-screens:plan.${tier}.benefit` }],
|
|
95
98
|
resolveCurrentTier,
|
|
99
|
+
...(isDocsVariant && { priceNoteKey: "gallery-screens:plan.price-note" }),
|
|
96
100
|
viewRoles: ["TenantAdmin", "SystemAdmin"],
|
|
97
101
|
successPath: GALLERY_PLAN_PATH,
|
|
98
102
|
cancelPath: GALLERY_PLAN_PATH,
|
|
@@ -115,12 +119,12 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
|
|
|
115
119
|
},
|
|
116
120
|
[GALLERY_MOR_PROVIDER]: {
|
|
117
121
|
checkoutNote: {
|
|
118
|
-
de: "Verkäufer ist
|
|
119
|
-
en: "Sold by
|
|
122
|
+
de: "Verkäufer ist Paddle · Rechnung und USt von Paddle",
|
|
123
|
+
en: "Sold by Paddle · invoice and VAT from Paddle",
|
|
120
124
|
},
|
|
121
125
|
vatNote: {
|
|
122
|
-
de: "Rechnung und Umsatzsteuer stellt
|
|
123
|
-
en: "
|
|
126
|
+
de: "Rechnung und Umsatzsteuer stellt Paddle als Merchant of Record.",
|
|
127
|
+
en: "Paddle, as merchant of record, issues the invoice and charges the VAT.",
|
|
124
128
|
},
|
|
125
129
|
},
|
|
126
130
|
},
|
|
@@ -164,7 +168,7 @@ const galleryProviderPlugin: SubscriptionProviderPlugin = {
|
|
|
164
168
|
// consent id fails, so a successful switch in the spec proves the consent went
|
|
165
169
|
// through.
|
|
166
170
|
const galleryMorProviderPlugin: SubscriptionProviderPlugin = {
|
|
167
|
-
displayName: "
|
|
171
|
+
displayName: "Paddle",
|
|
168
172
|
isBillingEnabled: async (ctx) => morEnabledTenants.has(ctx.user.tenantId),
|
|
169
173
|
verifyAndParseWebhook: async (rawBody) => JSON.parse(rawBody),
|
|
170
174
|
priceToTier: {
|
|
@@ -184,6 +188,7 @@ const galleryMorProviderPlugin: SubscriptionProviderPlugin = {
|
|
|
184
188
|
tax: 106,
|
|
185
189
|
currency: "eur",
|
|
186
190
|
credit: 333,
|
|
191
|
+
...(isDocsVariant && { recurringAmount: 2368, recurringTax: 378 }),
|
|
187
192
|
effectiveAt: "2027-01-15T00:00:00Z",
|
|
188
193
|
}),
|
|
189
194
|
applyPlanSwitch: async (_ctx, options) => {
|
|
@@ -218,6 +223,7 @@ export const galleryScreensFeature: FeatureDefinition = defineFeature("gallery-s
|
|
|
218
223
|
r.translations({
|
|
219
224
|
keys: {
|
|
220
225
|
"gallery:nav.billing": { de: "Abrechnung", en: "Billing" },
|
|
226
|
+
"gallery-screens:plan.price-note": { de: "zzgl. USt.", en: "plus VAT" },
|
|
221
227
|
"gallery-screens:plan.free.label": { de: "Free", en: "Free" },
|
|
222
228
|
"gallery-screens:plan.starter.label": { de: "Starter", en: "Starter" },
|
|
223
229
|
"gallery-screens:plan.pro.label": { de: "Pro", en: "Pro" },
|
|
@@ -30,11 +30,11 @@ const port = Number.parseInt(process.env["PORT"] ?? "4196", 10);
|
|
|
30
30
|
|
|
31
31
|
const BILLING_FEATURE = "billing-foundation";
|
|
32
32
|
const EMAIL_FEATURE = "channel-email";
|
|
33
|
-
// The stock app mounts the real Mollie and
|
|
33
|
+
// The stock app mounts the real Mollie and Paddle providers; the gallery's
|
|
34
34
|
// provider choice is the two mocks, which never call out.
|
|
35
35
|
const REAL_PROVIDER_FEATURES: ReadonlySet<string> = new Set([
|
|
36
36
|
"subscription-mollie",
|
|
37
|
-
"subscription-
|
|
37
|
+
"subscription-paddle",
|
|
38
38
|
]);
|
|
39
39
|
|
|
40
40
|
const features = [
|
|
@@ -316,7 +316,7 @@
|
|
|
316
316
|
},
|
|
317
317
|
{
|
|
318
318
|
"name": "billing-foundation",
|
|
319
|
-
"description": "Plugin host for subscription billing. Manages the `read_subscriptions` projection table and exposes 5 domain events (subscription created/updated/canceled, invoice paid/failed) appended by the foundation's own `billing-foundation:write:process-event` write-handler after provider plugins verify and normalize each webhook. Also manages a separate `read_payments` projection table (one row per one-off-payment) fed by its own `payment-received` event and `billing-foundation:write:process-payment-event` write-handler. Also ships `billing-foundation:write:create-checkout-session` and `billing-foundation:write:create-portal-session` write-handlers, a `billing-foundation:query:subscription:list` query handler, and a `createSubscriptionWebhookRoute` factory for the `/api/subscription/webhook/:providerName` extraRoute. `createBillingFoundationFeature({ baseUrl, catalog })` additionally derives a `billing-foundation:query:billing-plans` query, `start-plan-checkout`/`switch-plan` write-handlers, a `plan-switch-preview` query and a dormant billing-plans dashboard screen/panel from the catalog. Several providers can be mounted at once: new checkouts are offered by every enabled provider (enabled means keys set and billingLive on, or simply mounted for plugins without `isBillingEnabled`), the customer picks one in the plan panel and `start-plan-checkout` takes a `providerName`. An existing subscription stays with the provider that stored it, so the plan list and every switch follow that provider. A plan switch is either hosted (`createPlanSwitchSession` answers a url) or in-app (`previewPlanSwitch` shows the proration, `applyPlanSwitch` runs after the customer confirms; `switch-plan` then answers `{ applied: true }` and records the consent). `consumerProtection.providerTexts` overrides the checkout note and the VAT note per provider. Also ships `billing-foundation:write:sync-subscription` (pulls a provider plugin's live subscription state via `retrieveSubscription` and appends drift as a `subscription.updated` event, which catches changes made on the provider's own dashboard that never reached us as a webhook) and the `sync-subscriptions` job (manual-trigger + runOnBoot, perTenant) that dispatches it. Low-level building block: use `subscription-stripe`, `subscription-mollie` or `subscription-
|
|
319
|
+
"description": "Plugin host for subscription billing. Manages the `read_subscriptions` projection table and exposes 5 domain events (subscription created/updated/canceled, invoice paid/failed) appended by the foundation's own `billing-foundation:write:process-event` write-handler after provider plugins verify and normalize each webhook. Also manages a separate `read_payments` projection table (one row per one-off-payment) fed by its own `payment-received` event and `billing-foundation:write:process-payment-event` write-handler. Also ships `billing-foundation:write:create-checkout-session` and `billing-foundation:write:create-portal-session` write-handlers, a `billing-foundation:query:subscription:list` query handler, and a `createSubscriptionWebhookRoute` factory for the `/api/subscription/webhook/:providerName` extraRoute. `createBillingFoundationFeature({ baseUrl, catalog })` additionally derives a `billing-foundation:query:billing-plans` query, `start-plan-checkout`/`switch-plan` write-handlers, a `plan-switch-preview` query and a dormant billing-plans dashboard screen/panel from the catalog. Several providers can be mounted at once: new checkouts are offered by every enabled provider (enabled means keys set and billingLive on, or simply mounted for plugins without `isBillingEnabled`), the customer picks one in the plan panel and `start-plan-checkout` takes a `providerName`. An existing subscription stays with the provider that stored it, so the plan list and every switch follow that provider. A plan switch is either hosted (`createPlanSwitchSession` answers a url) or in-app (`previewPlanSwitch` shows the proration, `applyPlanSwitch` runs after the customer confirms; `switch-plan` then answers `{ applied: true }` and records the consent). `consumerProtection.providerTexts` overrides the checkout note and the VAT note per provider. Also ships `billing-foundation:write:sync-subscription` (pulls a provider plugin's live subscription state via `retrieveSubscription` and appends drift as a `subscription.updated` event, which catches changes made on the provider's own dashboard that never reached us as a webhook) and the `sync-subscriptions` job (manual-trigger + runOnBoot, perTenant) that dispatches it. Low-level building block: use `subscription-stripe`, `subscription-mollie` or `subscription-paddle` unless you are writing a new payment provider.",
|
|
320
320
|
"toggleableDefault": null,
|
|
321
321
|
"requires": [
|
|
322
322
|
"tenant-lifecycle",
|
|
@@ -1421,9 +1421,15 @@
|
|
|
1421
1421
|
"configKeys": [],
|
|
1422
1422
|
"secrets": [],
|
|
1423
1423
|
"writeHandlers": [
|
|
1424
|
+
"inbound-mail-foundation:write:acknowledge-quarantine",
|
|
1425
|
+
"inbound-mail-foundation:write:attach-message-content",
|
|
1424
1426
|
"inbound-mail-foundation:write:connect-account",
|
|
1425
1427
|
"inbound-mail-foundation:write:disconnect-account",
|
|
1426
1428
|
"inbound-mail-foundation:write:ingest-message",
|
|
1429
|
+
"inbound-mail-foundation:write:mark-credential-stored",
|
|
1430
|
+
"inbound-mail-foundation:write:reactivate-account",
|
|
1431
|
+
"inbound-mail-foundation:write:retry-quarantined-message",
|
|
1432
|
+
"inbound-mail-foundation:write:set-trusted-authserv-id",
|
|
1427
1433
|
"inbound-mail-foundation:write:update-account"
|
|
1428
1434
|
],
|
|
1429
1435
|
"uiHints": {
|
|
@@ -1434,7 +1440,7 @@
|
|
|
1434
1440
|
},
|
|
1435
1441
|
{
|
|
1436
1442
|
"name": "inbound-provider-imap",
|
|
1437
|
-
"description": "Registers the `\"imap\"` provider for `inbound-mail-foundation` using imapflow — password/app-password auth covers classic IMAP hosts without any OAuth (plus an XOAUTH2 fallback consuming an access token from the secret slot). Incremental sync via a UIDVALIDITY:lastUid cursor (UIDVALIDITY change triggers a cursor-invalid full resync), initial backfill bounded by the foundation backfill window, and live push via IMAP IDLE for the watch supervisor. Tenant admins connect a mailbox through the \"Connect mailbox\" form on the mail accounts list (`connect-mailbox`
|
|
1443
|
+
"description": "Registers the `\"imap\"` provider for `inbound-mail-foundation` using imapflow — password/app-password auth covers classic IMAP hosts without any OAuth (plus an XOAUTH2 fallback consuming an access token from the secret slot). Incremental sync via a UIDVALIDITY:lastUid cursor (UIDVALIDITY change triggers a cursor-invalid full resync), initial backfill bounded by the foundation backfill window, and live push via IMAP IDLE for the watch supervisor. Tenant admins connect a shared mailbox through the \"Connect mailbox\" form on the mail accounts list and members a personal one through the same form on \"My mail accounts\" (`connect-mailbox` and `connect-my-mailbox` check the IMAP login and the SMTP login of the outgoing server first, then creates the account and its credential secret in one transaction; `reconnect-mailbox` does the same check for an existing mailbox with a sign-in error and overwrites its credential secret); programmatic setups store the per-account connection JSON (host/port/secure/user/password, plus smtpHost/smtpPort for sending) in the account credential secret slot before the first sync. Implements the provider `send`: `sendMailFromAccount` sends over the SMTP server of the mailbox itself (implicit TLS, same login) with the mailbox address as From and stores a copy in its sent folder.",
|
|
1438
1444
|
"toggleableDefault": null,
|
|
1439
1445
|
"requires": [
|
|
1440
1446
|
"inbound-mail-foundation",
|
|
@@ -1454,6 +1460,7 @@
|
|
|
1454
1460
|
"secrets": [],
|
|
1455
1461
|
"writeHandlers": [
|
|
1456
1462
|
"inbound-provider-imap:write:connect-mailbox",
|
|
1463
|
+
"inbound-provider-imap:write:connect-my-mailbox",
|
|
1457
1464
|
"inbound-provider-imap:write:reconnect-mailbox"
|
|
1458
1465
|
],
|
|
1459
1466
|
"uiHints": {
|
|
@@ -1786,7 +1793,7 @@
|
|
|
1786
1793
|
},
|
|
1787
1794
|
{
|
|
1788
1795
|
"name": "managed-pages",
|
|
1789
|
-
"description": "Tenant-editable, server-rendered public pages with per-tenant branding. Stores one Markdown `
|
|
1796
|
+
"description": "Tenant-editable, server-rendered public pages with per-tenant branding. Stores one `page` (Markdown or editor-document HTML body, per `bodyFormat`) per `(tenantId, slug, lang)` in the `read_pages` entity table with a `published` gate plus `description`/`ogImage` SEO meta. Registers an anonymous `GET {basePath}/:slug` route that resolves the tenant from the request Host via the app-supplied `resolveApexTenant`, serves only published pages (drafts → 404), renders Markdown through the hardened `page-render` core and document bodies through the template-resolver document sanitizer, and isolates per-tenant content with `Vary: Host`. Ships TenantAdmin/SystemAdmin admin screens (`entityList` + `entityEdit`) backed by convention CRUD handlers (`managed-pages:write:page:{create,update,delete}`, `managed-pages:query:page:{list,detail}`); the app wires nav/workspace onto `managed-pages:screen:page-list`. Also exposes `managed-pages:query:by-tenant-published` (anonymous, SQL-filtered on `published`) for sitemap/discovery consumers such as the `seo` feature. Branding (via `config`, scope tenant): `branding-{title,description,site-url,accent-color,logo-url,layout-preset,custom-css}` keys with write-time validation (hex color, https URLs), a `configEdit` self-service screen (`managed-pages:screen:branding-settings`), and a `managed-pages:query:branding` read that the render path applies as scoped `:root` CSS vars + a logo/title header. Also exposes `managed-pages:write:set` (idempotent slug-keyed upsert, SystemAdmin cross-tenant via `tenantIdOverride`) as a provisioning API. Requires `config` + `anonymousAccess` wired at app bootstrap.",
|
|
1790
1797
|
"toggleableDefault": null,
|
|
1791
1798
|
"adoptsBundledLists": true,
|
|
1792
1799
|
"requires": [
|
|
@@ -2505,8 +2512,34 @@
|
|
|
2505
2512
|
}
|
|
2506
2513
|
},
|
|
2507
2514
|
{
|
|
2508
|
-
"name": "subscription-
|
|
2509
|
-
"description": "
|
|
2515
|
+
"name": "subscription-mollie",
|
|
2516
|
+
"description": "Mollie payment provider plugin for `billing-foundation`, covering the DACH/EU mid-market use case. Mount via `createSubscriptionMollieFeature({ apiKey, webhookUrl, priceToTier, priceToConfig })` — the factory validates that `priceToTier` and `priceToConfig` keys are identical at boot time. Implements `verifyAndParseWebhook` (lazy Mollie-API fetch + heuristic event-type mapping) and `createCheckoutSession` (customer + first-payment with `sequenceType=\"first\"`); `createPortalSession` and `cancelSubscription` are not available because Mollie has no customer portal and requires a `customerId` that the plugin contract does not carry.",
|
|
2517
|
+
"toggleableDefault": null,
|
|
2518
|
+
"requires": [
|
|
2519
|
+
"billing-foundation"
|
|
2520
|
+
],
|
|
2521
|
+
"optionalRequires": [],
|
|
2522
|
+
"configReads": [],
|
|
2523
|
+
"exposesApis": [],
|
|
2524
|
+
"usesApis": [],
|
|
2525
|
+
"extensionsUsed": [
|
|
2526
|
+
{
|
|
2527
|
+
"extensionName": "subscriptionProvider",
|
|
2528
|
+
"entityName": "mollie"
|
|
2529
|
+
}
|
|
2530
|
+
],
|
|
2531
|
+
"configKeys": [],
|
|
2532
|
+
"secrets": [],
|
|
2533
|
+
"writeHandlers": [],
|
|
2534
|
+
"uiHints": {
|
|
2535
|
+
"displayLabel": "Billing · Mollie",
|
|
2536
|
+
"category": "billing",
|
|
2537
|
+
"recommended": false
|
|
2538
|
+
}
|
|
2539
|
+
},
|
|
2540
|
+
{
|
|
2541
|
+
"name": "subscription-paddle",
|
|
2542
|
+
"description": "Paddle Billing provider plugin for `billing-foundation`. Paddle acts as merchant of record: it sells to the customer and issues invoice and VAT, so the consumer-protection texts for this provider come from `consumerProtection.providerTexts`. Reads its API key, webhook secret and tenant binding key from system config keys with `backing:\"secrets\"` (envelope-encrypted in the secrets store under the system tenant), the public Paddle.js `client-token`, a `billingLive` master switch and an `environment` key (sandbox | live). `environment` is its own key because Paddle serves sandbox and live from different API hosts and a live key must never hit the sandbox host or the other way round; `billingLive` only decides whether checkouts are offered at all. All of them are runtime config, so keys rotate and prod goes live without a redeploy, and the `mask` on each derives the sysadmin settings screen and nav. Mount via `createSubscriptionPaddleFeature({ priceToTier })`; the optional `apiKey`/`webhookSecret`/`tenantBindingKey`/`clientToken` options are env to config bridge fallbacks (`PADDLE_API_KEY`, `PADDLE_WEBHOOK_SECRET`, `PADDLE_TENANT_BINDING_KEY`, `PADDLE_CLIENT_TOKEN`). The plugin always mounts; `createCheckoutSession` throws `feature_disabled` unless `billingLive` is true, and billing counts as enabled only with API key and binding key present. Webhooks arrive at `/webhooks/subscription/paddle` when mounted through the server-runtime `billing.paddle` boot option, or at `/api/subscription/webhook/paddle` when `createSubscriptionWebhookRoute` is mounted by hand and are verified with the `Paddle-Signature` header (HMAC-SHA256 over `ts:rawBody` keyed with the raw secret, any `h1` may match, 5 minute tolerance). Paddle.js can open checkouts with arbitrary `customData` from the browser, so `custom_data.tenantId` is only trusted together with `tenantBinding`, an HMAC of the tenant id under the binding key; events without a valid binding, for prices outside `priceToTier` or without a tenant are dropped. Rotating the binding key makes the webhooks of existing subscriptions fail that check until they are re-bound. Paddle has no hosted checkout page, so the feature serves the Default Payment Link itself at `/billing/paddle-checkout` (the Default Payment Link in the Paddle dashboard must point to `<APP_BASE_URL>/billing/paddle-checkout`; it is the only source, because checkouts are created without a `checkout.url`). Create subscription prices in the Paddle catalog with quantity minimum and maximum 1; for one-off packs set the quantity limits on purpose, because the payment event carries `quantity` and the app has to multiply its credits by it: an anonymous page with its own CSP that loads Paddle.js from cdn.paddle.com, the one external script this framework allows, and opens the overlay for the transaction. Implements checkout, customer portal, cancel and resume, `retrievePrices` (batched, 10-minute TTL cache), `retrieveSubscription` for the sync job, and an in-app plan switch: `previewPlanSwitch` shows the proration before the customer confirms, `applyPlanSwitch` changes the plan without a hosted page and waits for the `subscription.updated` webhook to move the tier.",
|
|
2510
2543
|
"toggleableDefault": null,
|
|
2511
2544
|
"requires": [
|
|
2512
2545
|
"billing-foundation",
|
|
@@ -2520,13 +2553,13 @@
|
|
|
2520
2553
|
"extensionsUsed": [
|
|
2521
2554
|
{
|
|
2522
2555
|
"extensionName": "subscriptionProvider",
|
|
2523
|
-
"entityName": "
|
|
2556
|
+
"entityName": "paddle"
|
|
2524
2557
|
}
|
|
2525
2558
|
],
|
|
2526
2559
|
"configKeys": [
|
|
2527
2560
|
{
|
|
2528
2561
|
"key": "api-key",
|
|
2529
|
-
"qualifiedName": "subscription-
|
|
2562
|
+
"qualifiedName": "subscription-paddle:config:api-key",
|
|
2530
2563
|
"type": "text",
|
|
2531
2564
|
"scope": "system",
|
|
2532
2565
|
"default": null,
|
|
@@ -2547,7 +2580,7 @@
|
|
|
2547
2580
|
},
|
|
2548
2581
|
{
|
|
2549
2582
|
"key": "billing-live",
|
|
2550
|
-
"qualifiedName": "subscription-
|
|
2583
|
+
"qualifiedName": "subscription-paddle:config:billing-live",
|
|
2551
2584
|
"type": "boolean",
|
|
2552
2585
|
"scope": "system",
|
|
2553
2586
|
"default": false,
|
|
@@ -2567,17 +2600,38 @@
|
|
|
2567
2600
|
"SystemAdmin"
|
|
2568
2601
|
]
|
|
2569
2602
|
},
|
|
2603
|
+
{
|
|
2604
|
+
"key": "client-token",
|
|
2605
|
+
"qualifiedName": "subscription-paddle:config:client-token",
|
|
2606
|
+
"type": "text",
|
|
2607
|
+
"scope": "system",
|
|
2608
|
+
"default": null,
|
|
2609
|
+
"encrypted": false,
|
|
2610
|
+
"computed": false,
|
|
2611
|
+
"validated": false,
|
|
2612
|
+
"options": null,
|
|
2613
|
+
"bounds": null,
|
|
2614
|
+
"pattern": null,
|
|
2615
|
+
"writeRoles": [
|
|
2616
|
+
"SystemAdmin"
|
|
2617
|
+
],
|
|
2618
|
+
"readRoles": [
|
|
2619
|
+
"TenantAdmin",
|
|
2620
|
+
"Admin",
|
|
2621
|
+
"SystemAdmin"
|
|
2622
|
+
]
|
|
2623
|
+
},
|
|
2570
2624
|
{
|
|
2571
2625
|
"key": "environment",
|
|
2572
|
-
"qualifiedName": "subscription-
|
|
2626
|
+
"qualifiedName": "subscription-paddle:config:environment",
|
|
2573
2627
|
"type": "select",
|
|
2574
2628
|
"scope": "system",
|
|
2575
|
-
"default": "
|
|
2629
|
+
"default": "sandbox",
|
|
2576
2630
|
"encrypted": false,
|
|
2577
2631
|
"computed": false,
|
|
2578
2632
|
"validated": false,
|
|
2579
2633
|
"options": [
|
|
2580
|
-
"
|
|
2634
|
+
"sandbox",
|
|
2581
2635
|
"live"
|
|
2582
2636
|
],
|
|
2583
2637
|
"bounds": null,
|
|
@@ -2592,8 +2646,8 @@
|
|
|
2592
2646
|
]
|
|
2593
2647
|
},
|
|
2594
2648
|
{
|
|
2595
|
-
"key": "
|
|
2596
|
-
"qualifiedName": "subscription-
|
|
2649
|
+
"key": "tenant-binding-key",
|
|
2650
|
+
"qualifiedName": "subscription-paddle:config:tenant-binding-key",
|
|
2597
2651
|
"type": "text",
|
|
2598
2652
|
"scope": "system",
|
|
2599
2653
|
"default": null,
|
|
@@ -2611,38 +2665,33 @@
|
|
|
2611
2665
|
"Admin",
|
|
2612
2666
|
"SystemAdmin"
|
|
2613
2667
|
]
|
|
2614
|
-
}
|
|
2615
|
-
],
|
|
2616
|
-
"secrets": [],
|
|
2617
|
-
"writeHandlers": [],
|
|
2618
|
-
"uiHints": {
|
|
2619
|
-
"displayLabel": "Billing · Dodo Payments",
|
|
2620
|
-
"category": "billing",
|
|
2621
|
-
"recommended": false
|
|
2622
|
-
}
|
|
2623
|
-
},
|
|
2624
|
-
{
|
|
2625
|
-
"name": "subscription-mollie",
|
|
2626
|
-
"description": "Mollie payment provider plugin for `billing-foundation`, covering the DACH/EU mid-market use case. Mount via `createSubscriptionMollieFeature({ apiKey, webhookUrl, priceToTier, priceToConfig })` — the factory validates that `priceToTier` and `priceToConfig` keys are identical at boot time. Implements `verifyAndParseWebhook` (lazy Mollie-API fetch + heuristic event-type mapping) and `createCheckoutSession` (customer + first-payment with `sequenceType=\"first\"`); `createPortalSession` and `cancelSubscription` are not available because Mollie has no customer portal and requires a `customerId` that the plugin contract does not carry.",
|
|
2627
|
-
"toggleableDefault": null,
|
|
2628
|
-
"requires": [
|
|
2629
|
-
"billing-foundation"
|
|
2630
|
-
],
|
|
2631
|
-
"optionalRequires": [],
|
|
2632
|
-
"configReads": [],
|
|
2633
|
-
"exposesApis": [],
|
|
2634
|
-
"usesApis": [],
|
|
2635
|
-
"extensionsUsed": [
|
|
2668
|
+
},
|
|
2636
2669
|
{
|
|
2637
|
-
"
|
|
2638
|
-
"
|
|
2670
|
+
"key": "webhook-secret",
|
|
2671
|
+
"qualifiedName": "subscription-paddle:config:webhook-secret",
|
|
2672
|
+
"type": "text",
|
|
2673
|
+
"scope": "system",
|
|
2674
|
+
"default": null,
|
|
2675
|
+
"encrypted": true,
|
|
2676
|
+
"computed": false,
|
|
2677
|
+
"validated": false,
|
|
2678
|
+
"options": null,
|
|
2679
|
+
"bounds": null,
|
|
2680
|
+
"pattern": null,
|
|
2681
|
+
"writeRoles": [
|
|
2682
|
+
"SystemAdmin"
|
|
2683
|
+
],
|
|
2684
|
+
"readRoles": [
|
|
2685
|
+
"TenantAdmin",
|
|
2686
|
+
"Admin",
|
|
2687
|
+
"SystemAdmin"
|
|
2688
|
+
]
|
|
2639
2689
|
}
|
|
2640
2690
|
],
|
|
2641
|
-
"configKeys": [],
|
|
2642
2691
|
"secrets": [],
|
|
2643
2692
|
"writeHandlers": [],
|
|
2644
2693
|
"uiHints": {
|
|
2645
|
-
"displayLabel": "Billing ·
|
|
2694
|
+
"displayLabel": "Billing · Paddle",
|
|
2646
2695
|
"category": "billing",
|
|
2647
2696
|
"recommended": false
|
|
2648
2697
|
}
|
|
@@ -2775,7 +2824,7 @@
|
|
|
2775
2824
|
},
|
|
2776
2825
|
{
|
|
2777
2826
|
"name": "template-resolver",
|
|
2778
|
-
"description": "Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).\n\nReading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`. Saving a draft over an active template keeps the active version served and stores the edit beside it (column `draft`); `publish` makes it the served version, `discardDraft` / `discardDraftSystem` throws it away.\n\nEditing templates is for SystemAdmin only. A TenantAdmin edits only the templates the app unlocks at the mount site, `createTemplateResolverFeature({ tenantEditable: [{ kind: \"mail\", slug: \"status-update\", access: { roles: [\"TenantAdmin\"] } }] })`, and every write handler enforces that on the server (override, draft, publish, archive, reset, copy-locale, import, resources, `set`). Each entry carries the `access` roles allowed to edit it, because a bundled feature cannot know the host's roles. The same list decides delivery: a tenant override is served (mail, PDF, layouts, text blocks) only while its template is listed in tenantEditable; otherwise the system version is served. A locked override stays stored, a SystemAdmin sees it in the overview marked as not delivered, and listing the template makes it effective again. Tenant rows of a collection mounted with `access` are always served. A collection mounted without `access` is SystemAdmin only.\n\nText an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.\n\nHow a collection is edited follows from `contentFormat`: `plain` gives a text area, `
|
|
2827
|
+
"description": "Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).\n\nReading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`. Saving a draft over an active template keeps the active version served and stores the edit beside it (column `draft`); `publish` makes it the served version, `discardDraft` / `discardDraftSystem` throws it away.\n\nEditing templates is for SystemAdmin only. A TenantAdmin edits only the templates the app unlocks at the mount site, `createTemplateResolverFeature({ tenantEditable: [{ kind: \"mail\", slug: \"status-update\", access: { roles: [\"TenantAdmin\"] } }] })`, and every write handler enforces that on the server (override, draft, publish, archive, reset, copy-locale, import, resources, `set`). Each entry carries the `access` roles allowed to edit it, because a bundled feature cannot know the host's roles. The same list decides delivery: a tenant override is served (mail, PDF, layouts, text blocks) only while its template is listed in tenantEditable; otherwise the system version is served. A locked override stays stored, a SystemAdmin sees it in the overview marked as not delivered, and listing the template makes it effective again. Tenant rows of a collection mounted with `access` are always served. A collection mounted without `access` is SystemAdmin only.\n\nText an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.\n\nHow a collection is edited follows from `contentFormat`: `plain` gives a text area that stores text, `document` a WYSIWYG document editor that stores sanitized HTML (bold, italic, headings, lists, links, variable chips). Both offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning).\n\nA collection is tenant-wide by default. With `ownership: \"user\"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side.\n\n`validate` dry-runs the write pipeline for a draft (sanitizer, layout rules, placeholders) and reports what the template uses and misses. `export-bundle` / `import-bundle` (and `import-bundle-system`) move one template with all locales and resource files between tenants or environments; `copy-locale` starts a translation from an existing locale. Layouts are structured: a mail or PDF layout is `data-header` / `data-content` / `data-signature` / `data-footer` regions, and mjml layout source is system-only.\n\n`preview` renders a draft (or the stored template) with sample values taken from the `example` of each declared variable, through the same gates and the same mail renderer as a real send, and returns subject, html, `displayHtml` (the html with cid images as data URIs for an editor), text, attachments (inline images and files) and `checks` (images resolved, plain text present, size under 10 MiB, required placeholders used); nothing is saved or sent. `send-test` mails that rendering to the calling user's own address only, subject prefixed with `[TEST]`, at most 10 per user and hour; it needs `delivery`, `channel-email` and `renderer-mail-html` to be mounted. `send-test-status` tells an editor beforehand whether that send can work for the caller (`canSend`, else `reason` `no_channel` or `no_address`), never returning the address.\n\nImages used by a template (`<img data-resource=\"logo\">`) are uploaded with `resourceUpload`, which re-encodes them, stores a file and links it under the key; `resourceRemove` unlinks it. `linkedResources` is written only through these two handlers. With `purpose: \"attachment\"` and a `fileName`, `resourceUpload` stores a PDF, PNG or JPEG as a file attachment of a mail template (original bytes, at most 10) that the mail renderer attaches after the inline images. `resourceLinkPdf` instead links a `document-pdf` template under a key: every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale, under a file name pattern such as `invoice-{{number}}.pdf`; files and linked PDFs share the limit of 10.\n\nReplaces the former `text-content` feature; its blocks now live here as kind `text-block`.",
|
|
2779
2828
|
"toggleableDefault": null,
|
|
2780
2829
|
"requires": [],
|
|
2781
2830
|
"optionalRequires": [],
|