@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.
Files changed (226) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +15 -5
  3. package/packages/bundled-features/src/account-security/README.md +1 -1
  4. package/packages/bundled-features/src/account-security/changes.json +6 -0
  5. package/packages/bundled-features/src/account-security/feature.ts +9 -1
  6. package/packages/bundled-features/src/audit/event-classification.ts +1 -0
  7. package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
  8. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
  9. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
  10. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
  11. package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
  12. package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
  13. package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
  14. package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
  15. package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
  16. package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
  17. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
  18. package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
  19. package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
  20. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
  21. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
  22. package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
  23. package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
  24. package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
  25. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
  26. package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
  27. package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
  28. package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
  29. package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
  30. package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
  31. package/packages/bundled-features/src/cap-overview/changes.json +6 -0
  32. package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
  33. package/packages/bundled-features/src/cap-overview/types.ts +2 -0
  34. package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
  35. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
  36. package/packages/bundled-features/src/files/changes.json +18 -0
  37. package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
  38. package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
  39. package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
  40. package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
  41. package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
  42. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +143 -0
  43. package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
  44. package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
  45. package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
  46. package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
  47. package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
  48. package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
  49. package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
  50. package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
  51. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
  52. package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
  53. package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
  54. package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
  55. package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
  56. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
  57. package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
  58. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
  59. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
  60. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
  61. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
  62. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
  63. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
  64. package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
  65. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
  66. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
  67. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
  68. package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
  69. package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
  70. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
  71. package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
  72. package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
  73. package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
  74. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
  75. package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -2
  76. package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
  77. package/packages/bundled-features/src/inbound-mail-foundation/index.ts +93 -15
  78. package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
  79. package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
  80. package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
  81. package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
  82. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
  83. package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
  84. package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
  85. package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
  86. package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
  87. package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
  89. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
  90. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
  91. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
  92. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
  93. package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
  94. package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
  95. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
  96. package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +238 -89
  97. package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
  98. package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
  99. package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
  100. package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
  101. package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
  102. package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
  103. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
  104. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
  105. package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
  106. package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
  107. package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
  108. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
  109. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
  110. package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
  111. package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
  112. package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
  113. package/packages/bundled-features/src/inbound-provider-imap/changes.json +36 -0
  114. package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +124 -41
  115. package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
  116. package/packages/bundled-features/src/inbound-provider-imap/constants.ts +2 -1
  117. package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
  118. package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
  119. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +61 -136
  120. package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
  121. package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +9 -0
  122. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
  123. package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
  124. package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
  125. package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
  126. package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +4 -6
  127. package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +75 -45
  128. package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +19 -8
  129. package/packages/bundled-features/src/inbound-provider-imap/screens.ts +203 -109
  130. package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
  131. package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
  132. package/packages/bundled-features/src/inbound-provider-imap/watch-mailbox.ts +109 -0
  133. package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
  134. package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
  135. package/packages/bundled-features/src/jobs/changes.json +18 -0
  136. package/packages/bundled-features/src/managed-pages/changes.json +7 -0
  137. package/packages/bundled-features/src/managed-pages/constants.ts +7 -0
  138. package/packages/bundled-features/src/managed-pages/feature.ts +6 -4
  139. package/packages/bundled-features/src/managed-pages/handlers/by-slug.query.ts +2 -0
  140. package/packages/bundled-features/src/managed-pages/handlers/page-body-guard.ts +61 -0
  141. package/packages/bundled-features/src/managed-pages/handlers/page-body-issues.ts +26 -0
  142. package/packages/bundled-features/src/managed-pages/handlers/page-crud.ts +17 -10
  143. package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +15 -4
  144. package/packages/bundled-features/src/managed-pages/render-page-body.ts +28 -0
  145. package/packages/bundled-features/src/managed-pages/screens/page-screens.ts +6 -2
  146. package/packages/bundled-features/src/managed-pages/seeding.ts +13 -2
  147. package/packages/bundled-features/src/managed-pages/table.ts +6 -2
  148. package/packages/bundled-features/src/managed-pages/web/client-plugin.tsx +6 -4
  149. package/packages/bundled-features/src/managed-pages/web/constants.ts +14 -0
  150. package/packages/bundled-features/src/managed-pages/web/i18n.ts +14 -0
  151. package/packages/bundled-features/src/managed-pages/web/lossy-markdown.ts +71 -0
  152. package/packages/bundled-features/src/managed-pages/web/markdown-to-document.ts +27 -0
  153. package/packages/bundled-features/src/managed-pages/web/page-body-section.tsx +73 -0
  154. package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
  155. package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
  156. package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
  157. package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
  158. package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
  159. package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
  160. package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
  161. package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
  162. package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
  163. package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
  164. package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
  165. package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
  166. package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
  167. package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
  168. package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
  169. package/packages/bundled-features/src/template-resolver/README.md +2 -2
  170. package/packages/bundled-features/src/template-resolver/changes.json +20 -0
  171. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +16 -16
  172. package/packages/bundled-features/src/template-resolver/feature.ts +14 -1
  173. package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
  174. package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
  175. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +24 -6
  176. package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +32 -0
  177. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +1 -0
  178. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +9 -3
  179. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +13 -480
  180. package/packages/bundled-features/src/template-resolver/web/collection-entry-create-form.tsx +105 -0
  181. package/packages/bundled-features/src/template-resolver/web/collection-lookup.ts +33 -0
  182. package/packages/bundled-features/src/template-resolver/web/constants.ts +3 -0
  183. package/packages/bundled-features/src/template-resolver/web/content-field.tsx +61 -0
  184. package/packages/bundled-features/src/template-resolver/web/content-tree-provider.ts +256 -0
  185. package/packages/bundled-features/src/template-resolver/web/document-reference-name.ts +12 -0
  186. package/packages/bundled-features/src/template-resolver/web/document-schema.ts +3 -11
  187. package/packages/bundled-features/src/template-resolver/web/i18n.ts +9 -2
  188. package/packages/bundled-features/src/template-resolver/web/legacy-variable-text.ts +30 -0
  189. package/packages/bundled-features/src/template-resolver/web/tenant-override.ts +7 -0
  190. package/packages/bundled-features/src/template-resolver/web/text-block-editor.tsx +92 -0
  191. package/packages/bundled-features/src/template-resolver/web/use-collection-editor-context.ts +42 -0
  192. package/packages/bundled-features/src/template-resolver/web/use-collection-entry-create.ts +92 -0
  193. package/packages/bundled-features/src/template-resolver/web/use-content-write.ts +81 -0
  194. package/packages/bundled-features/src/template-resolver/web/use-text-block-editor.ts +127 -0
  195. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  196. package/packages/bundled-features/src/tier-engine/feature.ts +9 -1
  197. package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
  198. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
  199. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
  200. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
  201. package/samples/apps/use-all-bundled/feature-manifest.json +89 -40
  202. package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
  203. package/samples/apps/use-all-bundled/package.json +1 -0
  204. package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
  205. package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
  206. package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
  207. package/samples/apps/use-all-bundled/src/app/client.tsx +8 -1
  208. package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
  209. package/samples/apps/use-all-bundled/src/app/seed.ts +17 -2
  210. package/samples/apps/use-all-bundled/src/run-config.ts +19 -11
  211. package/samples/recipes/cross-feature-events/README.md +10 -0
  212. package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
  213. package/samples/recipes/dashboard-stats/README.md +8 -0
  214. package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
  215. package/samples/recipes/env-composition/README.md +2 -2
  216. package/samples/recipes/env-composition/src/env.ts +2 -2
  217. package/samples/recipes/inbound-mail-triage/README.md +8 -0
  218. package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
  219. package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
  220. package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
  221. package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
  222. package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
  223. package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
  224. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
  225. package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
  226. package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
@@ -0,0 +1,27 @@
1
+ import { isAllowedDocumentLinkHref } from "@cosmicdrift/kumiko-bundled-features/template-resolver/document-schema";
2
+ import { escapeHtml } from "@cosmicdrift/kumiko-renderer/headless";
3
+ import { Marked } from "marked";
4
+
5
+ // The server sanitizer rejects what it cannot keep, so the conversion drops it up front and the
6
+ // lossy banner stays true: a link outside the document link policy keeps its text, an image
7
+ // (no uploaded resource to point at) is dropped, and raw HTML is shown as text.
8
+ const converter = new Marked({ gfm: false, breaks: false });
9
+ converter.use({
10
+ renderer: {
11
+ html({ text }) {
12
+ return escapeHtml(text);
13
+ },
14
+ link({ href, tokens }) {
15
+ return isAllowedDocumentLinkHref(href) ? false : this.parser.parseInline(tokens);
16
+ },
17
+ image() {
18
+ return "";
19
+ },
20
+ },
21
+ });
22
+
23
+ export function markdownToDocumentHtml(markdown: string): string {
24
+ const html = converter.parse(markdown, { async: false });
25
+ if (typeof html !== "string") throw new Error("marked returned a promise for a sync parse");
26
+ return html;
27
+ }
@@ -0,0 +1,73 @@
1
+ import {
2
+ type ExtensionSectionProps,
3
+ usePrimitives,
4
+ useTranslation,
5
+ } from "@cosmicdrift/kumiko-renderer";
6
+ import { type ReactNode, useMemo, useState } from "react";
7
+ import { DocumentContentEditor } from "../../template-resolver/web/index.ts";
8
+ import { effectiveBodyFormat } from "./constants.ts";
9
+ import { detectLossyMarkdown, type LossyMarkdownFeature } from "./lossy-markdown.ts";
10
+ import { markdownToDocumentHtml } from "./markdown-to-document.ts";
11
+
12
+ const LOSSY_LABEL_KEYS: Readonly<Record<LossyMarkdownFeature, string>> = {
13
+ image: "managed-pages:body.lossy.image",
14
+ blockquote: "managed-pages:body.lossy.blockquote",
15
+ code: "managed-pages:body.lossy.code",
16
+ deepHeading: "managed-pages:body.lossy.deepHeading",
17
+ relativeLink: "managed-pages:body.lossy.relativeLink",
18
+ };
19
+
20
+ const BODY_EDITOR_ID = "managed-pages-page-body";
21
+
22
+ function stringValue(value: unknown): string {
23
+ return typeof value === "string" ? value : "";
24
+ }
25
+
26
+ // A Markdown body is converted for display only: nothing is patched until the
27
+ // user edits, so merely opening a page never turns it into a document.
28
+ export function PageBodySection({ values, patch }: ExtensionSectionProps): ReactNode {
29
+ const t = useTranslation();
30
+ const { Banner } = usePrimitives();
31
+ const body = stringValue(values?.["body"]);
32
+ const isMarkdown = effectiveBodyFormat(values?.["bodyFormat"]) === "markdown";
33
+ const [editedHtml, setEditedHtml] = useState<string | null>(null);
34
+ const [lossyAtFirstEdit, setLossyAtFirstEdit] = useState<readonly LossyMarkdownFeature[] | null>(
35
+ null,
36
+ );
37
+
38
+ const pristine = editedHtml === null;
39
+ const convertedHtml = useMemo(
40
+ () => (isMarkdown ? markdownToDocumentHtml(body) : body),
41
+ [isMarkdown, body],
42
+ );
43
+ const liveLossy = useMemo(
44
+ () => (pristine && isMarkdown ? detectLossyMarkdown(body) : []),
45
+ [pristine, isMarkdown, body],
46
+ );
47
+ const lossy = lossyAtFirstEdit ?? liveLossy;
48
+
49
+ const handleChange = (next: string): void => {
50
+ if (pristine) setLossyAtFirstEdit(liveLossy);
51
+ setEditedHtml(next);
52
+ patch?.({ body: next, bodyFormat: "document" });
53
+ };
54
+
55
+ return (
56
+ <>
57
+ {lossy.length > 0 && (
58
+ <Banner variant="warning">
59
+ {t("managed-pages:body.lossy.warning", {
60
+ features: lossy.map((feature) => t(LOSSY_LABEL_KEYS[feature])).join(", "),
61
+ })}
62
+ </Banner>
63
+ )}
64
+ <DocumentContentEditor
65
+ id={BODY_EDITOR_ID}
66
+ value={editedHtml ?? convertedHtml}
67
+ onChange={handleChange}
68
+ variables={[]}
69
+ readOnly={false}
70
+ />
71
+ </>
72
+ );
73
+ }
@@ -0,0 +1,15 @@
1
+ [
2
+ {
3
+ "version": "0.394.0",
4
+ "type": "improvement",
5
+ "title": "Add subscription-paddle, a Paddle Billing provider plugin",
6
+ "detail": "Checkout via the Paddle.js overlay on a page served by the feature 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 transactions 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. Also ships customer portal, cancel, resume, batched price lookup, in-app plan switch and tenant binding of checkouts through an HMAC in custom_data."
7
+ },
8
+ {
9
+ "version": "0.394.0",
10
+ "type": "breaking",
11
+ "title": "Remove subscription-dodo, replaced by subscription-paddle",
12
+ "detail": "Removed exports of @cosmicdrift/kumiko-bundled-features/subscription-dodo: DODO_BASE_URLS, DODO_PROVIDER_NAME, DodoEnvironment, DodoEnvironments, SUBSCRIPTION_DODO_FEATURE, SubscriptionDodoOptions, createSubscriptionDodoFeature, dodoEnvSchema, subscriptionDodoEnvSchema.",
13
+ "migration": "Replace createSubscriptionDodoFeature with createSubscriptionPaddleFeature from @cosmicdrift/kumiko-bundled-features/subscription-paddle and dodoEnvSchema with paddleEnvSchema. Map Paddle price ids (pri_...) to tiers in priceToTier, set PADDLE_API_KEY, PADDLE_WEBHOOK_SECRET, PADDLE_TENANT_BINDING_KEY and PADDLE_CLIENT_TOKEN (or the matching config keys) and point the Default Payment Link at /billing/paddle-checkout. Point the Paddle notification destination at /webhooks/subscription/paddle when billing is mounted through the server-runtime boot option (SUBSCRIPTION_WEBHOOK_PATH), or at /api/subscription/webhook/paddle when createSubscriptionWebhookRoute is mounted by hand. In server-runtime boot options, billing.dodo becomes billing.paddle. An app-own credit-pack env such as DODO_PRODUCT_PACKS becomes a list of Paddle pri_ ids; the pack size comes from the price or product custom_data.credits, which surfaces as ProviderPrice.metadata.credits. Existing Dodo subscriptions are not migrated; a subscription stays with the provider stored on it."
14
+ }
15
+ ]
@@ -0,0 +1,17 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import * as z from "zod";
3
+ import type { PaddleRuntimes } from "./runtime.ts";
4
+
5
+ // Anonymous on purpose: the checkout page is public. The answer is the public
6
+ // Paddle.js client token and the environment, nothing else.
7
+ export function makeCheckoutConfigQuery(readCheckoutConfig: PaddleRuntimes["readCheckoutConfig"]) {
8
+ return defineQueryHandler({
9
+ name: "checkout-config",
10
+ schema: z.object({}),
11
+ access: { roles: ["anonymous", "User", "TenantAdmin", "SystemAdmin"] },
12
+ rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
13
+ description:
14
+ "Answers the public Paddle.js client token and the Paddle environment for the checkout page; the client token is public by design and no other config leaves this handler.",
15
+ handler: async (_query, ctx) => readCheckoutConfig(ctx),
16
+ });
17
+ }
@@ -0,0 +1,107 @@
1
+ // Paddle plugin methods for checkout, portal, cancel and resume. Every response
2
+ // is validated with Zod; every non-2xx throws a PaddleHttpError.
3
+
4
+ import { SubscriptionCancelTimings } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
5
+ import * as z from "zod";
6
+ import type { PaddleCtxRuntime, PaddlePluginMethod } from "./runtime.ts";
7
+ import { signTenantBinding } from "./tenant-binding.ts";
8
+
9
+ export function paddleSubscriptionPath(providerSubscriptionId: string): string {
10
+ return `/subscriptions/${encodeURIComponent(providerSubscriptionId)}`;
11
+ }
12
+
13
+ function relativeTarget(url: string): string {
14
+ const parsed = new URL(url);
15
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
16
+ }
17
+
18
+ const checkoutResponseSchema = z.object({
19
+ data: z.object({
20
+ id: z.string(),
21
+ checkout: z.object({ url: z.string().nullable() }).nullish(),
22
+ }),
23
+ });
24
+
25
+ export function createPaddleCheckoutSession(
26
+ runtime: PaddleCtxRuntime,
27
+ ): PaddlePluginMethod<"createCheckoutSession"> {
28
+ return async (ctx, options) => {
29
+ // No Paddle transaction may be created while billing is not live.
30
+ await runtime.assertBillingLive(ctx);
31
+ const bindingKey = await runtime.tenantBindingKey(ctx);
32
+ // No `checkout.url` in the body: Paddle rejects unapproved domains and otherwise
33
+ // answers with the dashboard's Default Payment Link plus `?_ptxn=`.
34
+ const { data } = checkoutResponseSchema.parse(
35
+ await runtime.call(ctx, {
36
+ method: "POST",
37
+ path: "/transactions",
38
+ body: {
39
+ items: [{ price_id: options.priceId, quantity: 1 }],
40
+ ...(options.providerCustomerId && { customer_id: options.providerCustomerId }),
41
+ custom_data: {
42
+ tenantId: options.tenantId,
43
+ tenantBinding: signTenantBinding(bindingKey, options.tenantId),
44
+ ...(options.consentId && { consentId: options.consentId }),
45
+ },
46
+ },
47
+ }),
48
+ );
49
+ if (!data.checkout?.url) {
50
+ throw new Error("subscription-paddle: transaction was created without a checkout url");
51
+ }
52
+ const url = new URL(data.checkout.url);
53
+ url.searchParams.set("success", relativeTarget(options.successUrl));
54
+ url.searchParams.set("cancel", relativeTarget(options.cancelUrl));
55
+ if (options.locale) url.searchParams.set("locale", options.locale);
56
+ return { url: url.href };
57
+ };
58
+ }
59
+
60
+ const portalResponseSchema = z.object({
61
+ data: z.object({ urls: z.object({ general: z.object({ overview: z.string() }) }) }),
62
+ });
63
+
64
+ // Paddle's portal has no return url, so options.returnUrl is not used.
65
+ export function createPaddlePortalSession(
66
+ runtime: PaddleCtxRuntime,
67
+ ): PaddlePluginMethod<"createPortalSession"> {
68
+ return async (ctx, options) => {
69
+ const response = portalResponseSchema.parse(
70
+ await runtime.call(ctx, {
71
+ method: "POST",
72
+ path: `/customers/${encodeURIComponent(options.providerCustomerId)}/portal-sessions`,
73
+ body: {},
74
+ }),
75
+ );
76
+ return { url: response.data.urls.general.overview };
77
+ };
78
+ }
79
+
80
+ export function createPaddleCancelSubscription(
81
+ runtime: PaddleCtxRuntime,
82
+ ): PaddlePluginMethod<"cancelSubscription"> {
83
+ return async (ctx, options) => {
84
+ await runtime.call(ctx, {
85
+ method: "POST",
86
+ path: `${paddleSubscriptionPath(options.providerSubscriptionId)}/cancel`,
87
+ body: {
88
+ effective_from:
89
+ options.when === SubscriptionCancelTimings.periodEnd
90
+ ? "next_billing_period"
91
+ : "immediately",
92
+ },
93
+ });
94
+ };
95
+ }
96
+
97
+ export function createPaddleResumeSubscription(
98
+ runtime: PaddleCtxRuntime,
99
+ ): PaddlePluginMethod<"resumeSubscription"> {
100
+ return async (ctx, options) => {
101
+ await runtime.call(ctx, {
102
+ method: "PATCH",
103
+ path: paddleSubscriptionPath(options.providerSubscriptionId),
104
+ body: { scheduled_change: null },
105
+ });
106
+ };
107
+ }
@@ -0,0 +1,154 @@
1
+ // The Default Payment Link page. Paddle has no hosted checkout, so Paddle.js
2
+ // runs on this page and opens the overlay for the `?_ptxn=` transaction by
3
+ // itself. The page never reflects `_ptxn`.
4
+
5
+ import { createHash } from "node:crypto";
6
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { escapeHtml } from "@cosmicdrift/kumiko-renderer/headless";
8
+ import type { Context } from "hono";
9
+ import * as z from "zod";
10
+ import type { SystemQueryFn } from "../shared/index.ts";
11
+ import { PADDLE_CHECKOUT_CONFIG_QUERY, PaddleEnvironments } from "./constants.ts";
12
+
13
+ const PADDLE_JS_URL = "https://cdn.paddle.com/paddle/v2/paddle.js";
14
+ const MAX_REDIRECT_LENGTH = 2048;
15
+ const FALLBACK_REDIRECT = "/";
16
+ const LOCALE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;
17
+ // Control characters, which browsers strip or reinterpret inside URLs.
18
+ const CONTROL_CHARACTERS = /\p{Cc}/u;
19
+
20
+ // Static on purpose: its sha256 goes into the CSP, so nothing may be interpolated.
21
+ export const PADDLE_INIT_SCRIPT = `(function () {
22
+ var data = document.getElementById("kumiko-paddle-checkout").dataset;
23
+ var completed = false;
24
+ if (data.environment === "sandbox") Paddle.Environment.set("sandbox");
25
+ var settings = { displayMode: "overlay", successUrl: new URL(data.successUrl, location.origin).href };
26
+ if (data.locale) settings.locale = data.locale;
27
+ Paddle.Initialize({
28
+ token: data.clientToken,
29
+ checkout: { settings: settings },
30
+ eventCallback: function (event) {
31
+ if (event.name === "checkout.completed") completed = true;
32
+ if (event.name === "checkout.closed" && !completed) {
33
+ location.assign(new URL(data.cancelUrl, location.origin).href);
34
+ }
35
+ }
36
+ });
37
+ })();`;
38
+
39
+ const INIT_SCRIPT_HASH = createHash("sha256").update(PADDLE_INIT_SCRIPT).digest("base64");
40
+
41
+ export const PADDLE_CHECKOUT_CSP = [
42
+ "default-src 'none'",
43
+ `script-src 'sha256-${INIT_SCRIPT_HASH}' https://cdn.paddle.com`,
44
+ "style-src 'unsafe-inline' https://cdn.paddle.com",
45
+ "img-src data: https://*.paddle.com",
46
+ "connect-src https://*.paddle.com",
47
+ "frame-src https://buy.paddle.com https://sandbox-buy.paddle.com",
48
+ "frame-ancestors 'none'",
49
+ "base-uri 'none'",
50
+ "form-action 'none'",
51
+ "object-src 'none'",
52
+ ].join("; ");
53
+
54
+ // Own header set: `securePageHeaders` pins `script-src 'none'`. The global
55
+ // wrapper only fills headers that are absent, so these win.
56
+ function pageHeaders(): Record<string, string> {
57
+ return {
58
+ "content-type": "text/html; charset=utf-8",
59
+ "cache-control": "no-store",
60
+ "referrer-policy": "no-referrer",
61
+ "x-frame-options": "DENY",
62
+ "x-content-type-options": "nosniff",
63
+ "content-security-policy": PADDLE_CHECKOUT_CSP,
64
+ };
65
+ }
66
+
67
+ /** Same-origin path only: anything else would make the page an open redirector. */
68
+ export function safeRedirectPath(raw: string | undefined): string {
69
+ if (
70
+ raw === undefined ||
71
+ raw.length > MAX_REDIRECT_LENGTH ||
72
+ !raw.startsWith("/") ||
73
+ raw.startsWith("//") ||
74
+ raw.startsWith("/\\") ||
75
+ CONTROL_CHARACTERS.test(raw)
76
+ ) {
77
+ return FALLBACK_REDIRECT;
78
+ }
79
+ return raw;
80
+ }
81
+
82
+ export function safeLocale(raw: string | undefined): string | undefined {
83
+ return raw !== undefined && LOCALE_PATTERN.test(raw) ? raw : undefined;
84
+ }
85
+
86
+ type CheckoutPageValues = {
87
+ readonly clientToken: string;
88
+ readonly environment: string;
89
+ readonly successUrl: string;
90
+ readonly cancelUrl: string;
91
+ readonly locale: string | undefined;
92
+ };
93
+
94
+ export function renderCheckoutPage(values: CheckoutPageValues): string {
95
+ const attributesHtml = [
96
+ ["data-client-token", values.clientToken],
97
+ ["data-environment", values.environment],
98
+ ["data-success-url", values.successUrl],
99
+ ["data-cancel-url", values.cancelUrl],
100
+ ...(values.locale ? [["data-locale", values.locale]] : []),
101
+ ]
102
+ .map(([name, value]) => `${name}="${escapeHtml(value ?? "")}"`)
103
+ .join(" ");
104
+ return `<!doctype html>
105
+ <html lang="en">
106
+ <head>
107
+ <meta charset="utf-8">
108
+ <meta name="viewport" content="width=device-width, initial-scale=1">
109
+ <title>Checkout</title>
110
+ </head>
111
+ <body>
112
+ <div id="kumiko-paddle-checkout" ${attributesHtml}></div>
113
+ <noscript>JavaScript is required to complete the checkout.</noscript>
114
+ <script src="${PADDLE_JS_URL}"></script>
115
+ <script>${PADDLE_INIT_SCRIPT}</script>
116
+ </body>
117
+ </html>
118
+ `;
119
+ }
120
+
121
+ const checkoutConfigSchema = z.object({
122
+ clientToken: z.string().nullable(),
123
+ environment: z.string(),
124
+ });
125
+
126
+ export async function servePaddleCheckoutPage(
127
+ c: Context,
128
+ { systemQuery }: { readonly systemQuery: SystemQueryFn },
129
+ ): Promise<Response> {
130
+ let config: z.infer<typeof checkoutConfigSchema>;
131
+ try {
132
+ config = checkoutConfigSchema.parse(
133
+ await systemQuery(PADDLE_CHECKOUT_CONFIG_QUERY, {}, SYSTEM_TENANT_ID),
134
+ );
135
+ } catch {
136
+ return unavailable(c);
137
+ }
138
+ if (!config.clientToken) return unavailable(c);
139
+ const html = renderCheckoutPage({
140
+ clientToken: config.clientToken,
141
+ environment:
142
+ config.environment === PaddleEnvironments.live
143
+ ? PaddleEnvironments.live
144
+ : PaddleEnvironments.sandbox,
145
+ successUrl: safeRedirectPath(c.req.query("success")),
146
+ cancelUrl: safeRedirectPath(c.req.query("cancel")),
147
+ locale: safeLocale(c.req.query("locale")),
148
+ });
149
+ return new Response(html, { status: 200, headers: pageHeaders() });
150
+ }
151
+
152
+ function unavailable(c: Context): Response {
153
+ return c.text("checkout unavailable", 503, { "cache-control": "no-store" });
154
+ }
@@ -0,0 +1,78 @@
1
+ // Thin JSON-over-fetch client for the Paddle Billing REST API. All traffic goes
2
+ // through the injected `send`, which defaults to the SSRF-safe egress in
3
+ // feature.ts and is replaced by a mock in tests.
4
+
5
+ import { parseJsonOrThrow, parseJsonSafe } from "@cosmicdrift/kumiko-framework/utils";
6
+ import * as z from "zod";
7
+
8
+ export type PaddleSend = (url: string, init?: RequestInit) => Promise<Response>;
9
+
10
+ export type PaddleRequest = {
11
+ readonly method: "GET" | "POST" | "PATCH";
12
+ /** Path segments interpolated into it must already be encodeURIComponent'ed. */
13
+ readonly path: string;
14
+ readonly query?: Readonly<Record<string, string>>;
15
+ readonly body?: unknown;
16
+ };
17
+
18
+ export type PaddleTarget = { readonly baseUrl: string; readonly apiKey: string };
19
+
20
+ const PADDLE_API_VERSION = "1";
21
+
22
+ const paddleErrorBodySchema = z.object({ error: z.object({ type: z.string(), code: z.string() }) });
23
+ // Both values come from outside; anything beyond a plain identifier is dropped so
24
+ // no customer text reaches the logs.
25
+ const ERROR_IDENTIFIER = /^[a-z0-9_]{1,100}$/;
26
+
27
+ type PaddleErrorDetails = { readonly type?: string; readonly code?: string };
28
+
29
+ function paddleErrorDetails(bodyText: string): PaddleErrorDetails {
30
+ const parsed = paddleErrorBodySchema.safeParse(parseJsonSafe<unknown>(bodyText, undefined));
31
+ if (!parsed.success) return {};
32
+ const { type, code } = parsed.data.error;
33
+ return {
34
+ ...(ERROR_IDENTIFIER.test(type) && { type }),
35
+ ...(ERROR_IDENTIFIER.test(code) && { code }),
36
+ };
37
+ }
38
+
39
+ // Carries only the status and Paddle's error type and code, never `detail`, the
40
+ // rest of the body or the key: they can hold customer data or secrets.
41
+ export class PaddleHttpError extends Error {
42
+ readonly status: number;
43
+ readonly paddleErrorType?: string;
44
+ readonly paddleErrorCode?: string;
45
+ constructor(request: PaddleRequest, status: number, details: PaddleErrorDetails = {}) {
46
+ const suffix = [details.type && `type ${details.type}`, details.code && `code ${details.code}`]
47
+ .filter(Boolean)
48
+ .join(", ");
49
+ super(
50
+ `subscription-paddle: ${request.method} ${request.path} failed with status ${status}${suffix ? ` (${suffix})` : ""}`,
51
+ );
52
+ this.name = "PaddleHttpError";
53
+ this.status = status;
54
+ if (details.type) this.paddleErrorType = details.type;
55
+ if (details.code) this.paddleErrorCode = details.code;
56
+ }
57
+ }
58
+
59
+ export function createPaddleHttp(send: PaddleSend) {
60
+ return async (target: PaddleTarget, request: PaddleRequest): Promise<unknown> => {
61
+ const query = request.query ? `?${new URLSearchParams(request.query).toString()}` : "";
62
+ const response = await send(`${target.baseUrl}${request.path}${query}`, {
63
+ method: request.method,
64
+ headers: {
65
+ Authorization: `Bearer ${target.apiKey}`,
66
+ Accept: "application/json",
67
+ "Content-Type": "application/json",
68
+ "Paddle-Version": PADDLE_API_VERSION,
69
+ },
70
+ ...(request.body !== undefined && { body: JSON.stringify(request.body) }),
71
+ });
72
+ const text = await response.text();
73
+ if (!response.ok) throw new PaddleHttpError(request, response.status, paddleErrorDetails(text));
74
+ return text.length === 0
75
+ ? null
76
+ : parseJsonOrThrow<unknown>(text, "subscription-paddle response");
77
+ };
78
+ }
@@ -0,0 +1,32 @@
1
+ export const SUBSCRIPTION_PADDLE_FEATURE = "subscription-paddle" as const;
2
+
3
+ // Must match the last path segment of the webhook URL (`/webhooks/subscription/paddle` via the boot option).
4
+ export const PADDLE_PROVIDER_NAME = "paddle" as const;
5
+
6
+ // Paddle has no hosted checkout page, so the feature serves the Default Payment
7
+ // Link itself; the same path goes into the Paddle dashboard.
8
+ export const PADDLE_CHECKOUT_PATH = "/billing/paddle-checkout" as const;
9
+
10
+ // Config-key short-names, qualified to `subscription-paddle:config:<name>` at
11
+ // registry-build. api-key, webhook-secret and tenant-binding-key are backing:"secrets".
12
+ export const PADDLE_API_KEY_CONFIG = "api-key" as const;
13
+ export const PADDLE_WEBHOOK_SECRET_CONFIG = "webhook-secret" as const;
14
+ export const PADDLE_TENANT_BINDING_KEY_CONFIG = "tenant-binding-key" as const;
15
+ export const PADDLE_CLIENT_TOKEN_CONFIG = "client-token" as const;
16
+ export const PADDLE_ENVIRONMENT_CONFIG = "environment" as const;
17
+ export const PADDLE_BILLING_LIVE_CONFIG = "billingLive" as const;
18
+
19
+ export const PADDLE_CHECKOUT_CONFIG_QUERY = `${SUBSCRIPTION_PADDLE_FEATURE}:query:checkout-config`;
20
+
21
+ // Separate from billingLive on purpose: staging runs Paddle's sandbox with
22
+ // billingLive on, production flips both.
23
+ export const PaddleEnvironments = {
24
+ sandbox: "sandbox",
25
+ live: "live",
26
+ } as const;
27
+ export type PaddleEnvironment = (typeof PaddleEnvironments)[keyof typeof PaddleEnvironments];
28
+
29
+ export const PADDLE_BASE_URLS: Readonly<Record<PaddleEnvironment, string>> = {
30
+ sandbox: "https://sandbox-api.paddle.com",
31
+ live: "https://api.paddle.com",
32
+ };