@cosmicdrift/kumiko-samples 0.393.0 → 0.394.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 (186) 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 +30 -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 +75 -49
  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-inmemory/feature.ts +75 -3
  133. package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
  134. package/packages/bundled-features/src/jobs/changes.json +18 -0
  135. package/packages/bundled-features/src/managed-pages/web/i18n.ts +1 -0
  136. package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
  137. package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
  138. package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
  139. package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
  140. package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
  141. package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
  142. package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
  143. package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
  144. package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
  145. package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
  146. package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
  147. package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
  148. package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
  149. package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
  150. package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
  151. package/packages/bundled-features/src/template-resolver/changes.json +12 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
  153. package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
  154. package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +25 -0
  155. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +218 -117
  156. package/packages/bundled-features/src/template-resolver/web/i18n.ts +1 -2
  157. package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
  158. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
  159. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
  160. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
  161. package/samples/apps/use-all-bundled/feature-manifest.json +87 -38
  162. package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
  163. package/samples/apps/use-all-bundled/package.json +1 -0
  164. package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
  165. package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
  166. package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
  167. package/samples/apps/use-all-bundled/src/app/client.tsx +2 -0
  168. package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
  169. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -1
  170. package/samples/apps/use-all-bundled/src/run-config.ts +7 -5
  171. package/samples/recipes/cross-feature-events/README.md +10 -0
  172. package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
  173. package/samples/recipes/dashboard-stats/README.md +8 -0
  174. package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
  175. package/samples/recipes/env-composition/README.md +2 -2
  176. package/samples/recipes/env-composition/src/env.ts +2 -2
  177. package/samples/recipes/inbound-mail-triage/README.md +8 -0
  178. package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
  179. package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
  180. package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
  181. package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
  182. package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
  183. package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
  184. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
  185. package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
  186. package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
@@ -1,125 +1,219 @@
1
1
  import {
2
2
  IMAP_CONNECT_SCREEN_ID,
3
3
  IMAP_RECONNECT_SCREEN_ID,
4
+ type InboundMailRoles,
4
5
  MAIL_ACCOUNT_LIST_SCREEN_QN,
6
+ MY_IMAP_CONNECT_SCREEN_ID,
7
+ MY_IMAP_RECONNECT_SCREEN_ID,
8
+ MY_MAIL_ACCOUNT_LIST_SCREEN_QN,
5
9
  } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
6
10
  import { i18nKey } from "@cosmicdrift/kumiko-framework/engine";
7
11
  import type { ActionFormScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
8
12
  import { ImapHandlers, ImapQueries, SMTP_SUBMISSION_PORT } from "./constants.ts";
9
13
 
10
- // Reached from the toolbar of the foundation's mail account list, never from the nav.
11
- export const imapConnectScreen: ActionFormScreenDefinition = {
12
- id: IMAP_CONNECT_SCREEN_ID,
13
- type: "actionForm",
14
- agent: { expose: false },
15
- handler: ImapHandlers.connectMailbox,
16
- fields: {
17
- displayName: { type: "text", required: true, maxLength: 200 },
18
- address: { type: "text", required: true, maxLength: 320, format: "email" },
19
- host: { type: "text", required: true, maxLength: 255 },
20
- port: {
21
- type: "number",
22
- integer: true,
23
- grouping: false,
24
- required: true,
25
- min: 1,
26
- max: 65535,
27
- default: 993,
28
- },
29
- user: { type: "text", required: true, maxLength: 320 },
30
- password: { type: "text", required: true, format: "password", sensitive: true },
31
- smtpHost: { type: "text", maxLength: 255 },
32
- smtpPort: {
33
- type: "number",
34
- integer: true,
35
- grouping: false,
36
- required: true,
37
- min: 1,
38
- max: 65535,
39
- default: SMTP_SUBMISSION_PORT,
40
- },
41
- },
42
- layout: {
43
- sections: [
44
- // The name-based icon guess reads "address" as a postal address.
45
- { fields: ["displayName", { field: "address", icon: "mail" }] },
46
- {
47
- title: i18nKey("inboundMail.imap.connect.server"),
48
- fields: ["host", "port", "user", "password"],
14
+ // The admin form connects a shared mailbox, the member form a personal one. The member form carries
15
+ // no sender-check field because that policy is tenant-wide and only admins set it.
16
+ function createConnectScreen(variant: {
17
+ readonly id: string;
18
+ readonly handler: string;
19
+ readonly roles: readonly string[];
20
+ readonly returnTarget: string;
21
+ readonly withSenderCheck: boolean;
22
+ }): ActionFormScreenDefinition {
23
+ return {
24
+ id: variant.id,
25
+ type: "actionForm",
26
+ agent: { expose: false },
27
+ handler: variant.handler,
28
+ fields: {
29
+ displayName: { type: "text", required: true, maxLength: 200 },
30
+ address: { type: "text", required: true, maxLength: 320, format: "email" },
31
+ host: { type: "text", required: true, maxLength: 255 },
32
+ port: {
33
+ type: "number",
34
+ integer: true,
35
+ grouping: false,
36
+ required: true,
37
+ min: 1,
38
+ max: 65535,
39
+ default: 993,
49
40
  },
50
- {
51
- title: i18nKey("inboundMail.imap.connect.outgoing"),
52
- description: i18nKey("inboundMail.imap.connect.outgoingHint"),
53
- fields: ["smtpHost", "smtpPort"],
41
+ user: { type: "text", required: true, maxLength: 320 },
42
+ password: { type: "text", required: true, format: "password", sensitive: true },
43
+ smtpHost: { type: "text", maxLength: 255 },
44
+ smtpPort: {
45
+ type: "number",
46
+ integer: true,
47
+ grouping: false,
48
+ required: true,
49
+ min: 1,
50
+ max: 65535,
51
+ default: SMTP_SUBMISSION_PORT,
54
52
  },
55
- ],
56
- },
57
- submitLabel: i18nKey("inboundMail.imap.connect.submit"),
58
- redirect: MAIL_ACCOUNT_LIST_SCREEN_QN,
59
- cancelTarget: MAIL_ACCOUNT_LIST_SCREEN_QN,
60
- access: { roles: ["SystemAdmin", "TenantAdmin"] },
61
- dormant: true,
62
- };
53
+ ...(variant.withSenderCheck && { trustedAuthservId: { type: "text", maxLength: 255 } }),
54
+ },
55
+ layout: {
56
+ sections: [
57
+ // The name-based icon guess reads "address" as a postal address.
58
+ { fields: ["displayName", { field: "address", icon: "mail" }] },
59
+ {
60
+ title: i18nKey("inboundMail.imap.connect.server"),
61
+ fields: ["host", "port", "user", "password"],
62
+ },
63
+ {
64
+ title: i18nKey("inboundMail.imap.connect.outgoing"),
65
+ description: i18nKey("inboundMail.imap.connect.outgoingHint"),
66
+ fields: ["smtpHost", "smtpPort"],
67
+ },
68
+ ...(variant.withSenderCheck
69
+ ? [
70
+ {
71
+ title: i18nKey("inboundMail.imap.connect.senderCheck"),
72
+ description: i18nKey("inboundMail.imap.connect.senderCheckHint"),
73
+ fields: ["trustedAuthservId"],
74
+ },
75
+ ]
76
+ : []),
77
+ ],
78
+ },
79
+ submitLabel: i18nKey("inboundMail.imap.connect.submit"),
80
+ redirect: variant.returnTarget,
81
+ cancelTarget: variant.returnTarget,
82
+ access: { roles: [...variant.roles] },
83
+ dormant: true,
84
+ };
85
+ }
86
+
87
+ // Reached from the toolbar of the foundation's mail account list, never from the nav.
88
+ export const createImapConnectScreen = ({
89
+ adminRoles,
90
+ }: InboundMailRoles): ActionFormScreenDefinition =>
91
+ createConnectScreen({
92
+ id: IMAP_CONNECT_SCREEN_ID,
93
+ handler: ImapHandlers.connectMailbox,
94
+ roles: adminRoles,
95
+ returnTarget: MAIL_ACCOUNT_LIST_SCREEN_QN,
96
+ withSenderCheck: true,
97
+ });
98
+
99
+ // Reached from the toolbar of "My mail accounts".
100
+ export const createMyImapConnectScreen = ({
101
+ memberRoles,
102
+ }: InboundMailRoles): ActionFormScreenDefinition =>
103
+ createConnectScreen({
104
+ id: MY_IMAP_CONNECT_SCREEN_ID,
105
+ handler: ImapHandlers.connectMyMailbox,
106
+ roles: memberRoles,
107
+ returnTarget: MY_MAIL_ACCOUNT_LIST_SCREEN_QN,
108
+ withSenderCheck: false,
109
+ });
63
110
 
64
111
  // Reached from the "Reconnect" row action of an account with a sign-in error. The record id comes
65
112
  // from the path; the load query returns the stored server settings, never the password.
66
- export const imapReconnectScreen: ActionFormScreenDefinition = {
67
- id: IMAP_RECONNECT_SCREEN_ID,
68
- type: "actionForm",
69
- agent: { expose: false },
70
- handler: ImapHandlers.reconnectMailbox,
71
- load: { query: ImapQueries.reconnectPrefill, idParam: "accountId" },
72
- fields: {
73
- accountId: { type: "text", required: true },
74
- displayName: { type: "text", maxLength: 200 },
75
- host: { type: "text", required: true, maxLength: 255 },
76
- port: {
77
- type: "number",
78
- integer: true,
79
- grouping: false,
80
- required: true,
81
- min: 1,
82
- max: 65535,
83
- default: 993,
84
- },
85
- user: { type: "text", required: true, maxLength: 320 },
86
- password: { type: "text", required: true, format: "password", sensitive: true },
87
- smtpHost: { type: "text", maxLength: 255 },
88
- smtpPort: {
89
- type: "number",
90
- integer: true,
91
- grouping: false,
92
- required: true,
93
- min: 1,
94
- max: 65535,
95
- default: SMTP_SUBMISSION_PORT,
96
- },
97
- },
98
- layout: {
99
- sections: [
100
- {
101
- title: i18nKey("inboundMail.imap.connect.server"),
102
- description: i18nKey("inboundMail.imap.reconnect.hint"),
103
- // The id only travels in the payload; the admin recognises the mailbox by its name.
104
- fields: [
105
- { field: "accountId", visible: false },
106
- { field: "displayName", readOnly: true, submit: false },
107
- "host",
108
- "port",
109
- "user",
110
- "password",
111
- ],
113
+ // The admin and the owner variant share this form; the owner's carries no sender-check field
114
+ // because that policy is tenant-wide and only admins set it.
115
+ function createReconnectScreen(variant: {
116
+ readonly id: string;
117
+ readonly roles: readonly string[];
118
+ readonly returnTarget: string;
119
+ readonly withSenderCheck: boolean;
120
+ }): ActionFormScreenDefinition {
121
+ return {
122
+ id: variant.id,
123
+ type: "actionForm",
124
+ agent: { expose: false },
125
+ handler: ImapHandlers.reconnectMailbox,
126
+ load: { query: ImapQueries.reconnectPrefill, idParam: "accountId" },
127
+ fields: {
128
+ accountId: { type: "text", required: true },
129
+ displayName: { type: "text", maxLength: 200 },
130
+ host: { type: "text", required: true, maxLength: 255 },
131
+ port: {
132
+ type: "number",
133
+ integer: true,
134
+ grouping: false,
135
+ required: true,
136
+ min: 1,
137
+ max: 65535,
138
+ default: 993,
112
139
  },
113
- {
114
- title: i18nKey("inboundMail.imap.connect.outgoing"),
115
- description: i18nKey("inboundMail.imap.connect.outgoingHint"),
116
- fields: ["smtpHost", "smtpPort"],
140
+ user: { type: "text", required: true, maxLength: 320 },
141
+ password: { type: "text", required: true, format: "password", sensitive: true },
142
+ smtpHost: { type: "text", maxLength: 255 },
143
+ smtpPort: {
144
+ type: "number",
145
+ integer: true,
146
+ grouping: false,
147
+ required: true,
148
+ min: 1,
149
+ max: 65535,
150
+ default: SMTP_SUBMISSION_PORT,
117
151
  },
118
- ],
119
- },
120
- submitLabel: i18nKey("inboundMail.imap.reconnect.submit"),
121
- redirect: MAIL_ACCOUNT_LIST_SCREEN_QN,
122
- cancelTarget: MAIL_ACCOUNT_LIST_SCREEN_QN,
123
- access: { roles: ["SystemAdmin", "TenantAdmin"] },
124
- dormant: true,
125
- };
152
+ ...(variant.withSenderCheck && { trustedAuthservId: { type: "text", maxLength: 255 } }),
153
+ },
154
+ layout: {
155
+ sections: [
156
+ {
157
+ title: i18nKey("inboundMail.imap.connect.server"),
158
+ description: i18nKey("inboundMail.imap.reconnect.hint"),
159
+ // The id only travels in the payload; the user recognises the mailbox by its name.
160
+ fields: [
161
+ { field: "accountId", visible: false },
162
+ { field: "displayName", readOnly: true, submit: false },
163
+ "host",
164
+ "port",
165
+ "user",
166
+ "password",
167
+ ],
168
+ },
169
+ {
170
+ title: i18nKey("inboundMail.imap.connect.outgoing"),
171
+ description: i18nKey("inboundMail.imap.connect.outgoingHint"),
172
+ fields: ["smtpHost", "smtpPort"],
173
+ },
174
+ ...(variant.withSenderCheck
175
+ ? [
176
+ {
177
+ title: i18nKey("inboundMail.imap.connect.senderCheck"),
178
+ description: i18nKey("inboundMail.imap.connect.senderCheckHint"),
179
+ fields: ["trustedAuthservId"],
180
+ },
181
+ ]
182
+ : []),
183
+ ],
184
+ },
185
+ submitLabel: i18nKey("inboundMail.imap.reconnect.submit"),
186
+ redirect: variant.returnTarget,
187
+ cancelTarget: variant.returnTarget,
188
+ access: { roles: [...variant.roles] },
189
+ dormant: true,
190
+ };
191
+ }
192
+
193
+ export const createImapReconnectScreen = ({
194
+ adminRoles,
195
+ }: InboundMailRoles): ActionFormScreenDefinition =>
196
+ createReconnectScreen({
197
+ id: IMAP_RECONNECT_SCREEN_ID,
198
+ roles: adminRoles,
199
+ returnTarget: MAIL_ACCOUNT_LIST_SCREEN_QN,
200
+ withSenderCheck: true,
201
+ });
202
+
203
+ // Reached from the "Reactivate" row action of "My mail accounts".
204
+ export const createMyImapReconnectScreen = ({
205
+ memberRoles,
206
+ }: InboundMailRoles): ActionFormScreenDefinition =>
207
+ createReconnectScreen({
208
+ id: MY_IMAP_RECONNECT_SCREEN_ID,
209
+ roles: memberRoles,
210
+ returnTarget: MY_MAIL_ACCOUNT_LIST_SCREEN_QN,
211
+ withSenderCheck: false,
212
+ });
213
+
214
+ export const createImapScreens = (roles: InboundMailRoles): ActionFormScreenDefinition[] => [
215
+ createImapConnectScreen(roles),
216
+ createImapReconnectScreen(roles),
217
+ createMyImapConnectScreen(roles),
218
+ createMyImapReconnectScreen(roles),
219
+ ];
@@ -9,8 +9,9 @@ import {
9
9
  type MailAccountRecord,
10
10
  MailSendNotConfiguredError,
11
11
  MailSendRejectedError,
12
+ type OutboundAttachment,
12
13
  type OutboundMailMessage,
13
- type OutboundMailSent,
14
+ type ProviderSentMail,
14
15
  } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
15
16
  import MailComposer from "nodemailer/lib/mail-composer";
16
17
  import { FEATURE_NAME } from "./constants.ts";
@@ -37,7 +38,8 @@ export async function sendFromImapAccount(
37
38
  ctx: InboundMailContext,
38
39
  account: MailAccountRecord,
39
40
  message: OutboundMailMessage,
40
- ): Promise<OutboundMailSent> {
41
+ attachments: readonly OutboundAttachment[],
42
+ ): Promise<ProviderSentMail> {
41
43
  const doc = await readCredentialDocument(ctx, account);
42
44
  if (!doc.smtpHost || !doc.password) {
43
45
  throw new MailSendNotConfiguredError(
@@ -55,6 +57,13 @@ export async function sendFromImapAccount(
55
57
  ...(message.html !== undefined && { html: message.html }),
56
58
  ...(message.inReplyTo && { inReplyTo: message.inReplyTo }),
57
59
  ...(message.references && { references: [...message.references] }),
60
+ ...(attachments.length > 0 && {
61
+ attachments: attachments.map((attachment) => ({
62
+ filename: attachment.fileName,
63
+ contentType: attachment.mimeType,
64
+ content: Buffer.from(attachment.data),
65
+ })),
66
+ }),
58
67
  }).compile();
59
68
  const raw = await mime.build();
60
69
 
@@ -0,0 +1,36 @@
1
+ import {
2
+ fromDomainOf,
3
+ parseSenderAuthentication,
4
+ type SenderAuthentication,
5
+ } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
6
+ import type { ParsedMail } from "mailparser";
7
+
8
+ const HEADER_NAME = "authentication-results";
9
+
10
+ // headerLines keeps every occurrence in file order, topmost first. Unlike `headers`, which merges
11
+ // repeated headers, the order is what the trust rule depends on.
12
+ function authenticationResultValues(parsed: ParsedMail): string[] {
13
+ return parsed.headerLines
14
+ .filter((header) => header.key === HEADER_NAME)
15
+ .map((header) => header.line.slice(header.line.indexOf(":") + 1));
16
+ }
17
+
18
+ // A From header with addresses of several domains cannot be bound to one DMARC verdict.
19
+ function singleFromDomain(parsed: ParsedMail): string | null {
20
+ const domains = new Set(
21
+ (parsed.from?.value ?? []).map((address) => fromDomainOf(address.address ?? "")),
22
+ );
23
+ const [only] = domains;
24
+ return domains.size === 1 && only ? only : null;
25
+ }
26
+
27
+ export function senderAuthenticationOf(
28
+ parsed: ParsedMail,
29
+ trustedAuthservId: string | null,
30
+ ): SenderAuthentication | null {
31
+ return parseSenderAuthentication(
32
+ authenticationResultValues(parsed),
33
+ trustedAuthservId,
34
+ singleFromDomain(parsed),
35
+ );
36
+ }
@@ -13,11 +13,13 @@
13
13
 
14
14
  import {
15
15
  InboundAuthError,
16
+ type InboundContentFetchResult,
16
17
  type InboundFetchResult,
17
18
  type InboundMailContext,
18
19
  type InboundMailProviderPlugin,
19
20
  type MailAccountRecord,
20
21
  type RawInboundMessage,
22
+ type RejectedInboundMessage,
21
23
  type SyncCursorPayload,
22
24
  } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
23
25
  import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
@@ -36,11 +38,21 @@ type WatchHandlers = {
36
38
  readonly onError: (err: unknown) => void;
37
39
  };
38
40
 
41
+ type LogEntry =
42
+ | { readonly message: RawInboundMessage }
43
+ | { readonly rejected: RejectedInboundMessage };
44
+
39
45
  type AccountState = {
40
- readonly log: RawInboundMessage[];
46
+ readonly log: LogEntry[];
47
+ // What fetchContent serves; a message that is not in here counts as gone from the server.
48
+ readonly fullContent: Map<string, RawInboundMessage>;
49
+ readonly contentFetches: string[][];
41
50
  watch: WatchHandlers | null;
51
+ watchedTrustedAuthservId: string | null | undefined;
42
52
  nextFetchError: unknown;
43
53
  nextVerifyError: unknown;
54
+ hangingFetch: Promise<void> | null;
55
+ lastFetchSignal: AbortSignal | undefined;
44
56
  credentialSecretKey: string | null;
45
57
  };
46
58
 
@@ -51,9 +63,14 @@ function stateFor(accountId: string): AccountState {
51
63
  if (!state) {
52
64
  state = {
53
65
  log: [],
66
+ fullContent: new Map(),
67
+ contentFetches: [],
54
68
  watch: null,
69
+ watchedTrustedAuthservId: undefined,
55
70
  nextFetchError: null,
56
71
  nextVerifyError: null,
72
+ hangingFetch: null,
73
+ lastFetchSignal: undefined,
57
74
  credentialSecretKey: null,
58
75
  };
59
76
  accounts.set(accountId, state);
@@ -66,10 +83,25 @@ function stateFor(accountId: string): AccountState {
66
83
  * cursor — the foundation's deduplication makes this idempotent. */
67
84
  export async function seedInboundMessage(accountId: string, msg: RawInboundMessage): Promise<void> {
68
85
  const state = stateFor(accountId);
69
- state.log.push(msg);
86
+ state.log.push({ message: msg });
70
87
  if (state.watch) await state.watch.onMessages([msg]);
71
88
  }
72
89
 
90
+ /** Test helper: a mail the provider cannot parse; fetch reports it as rejected and moves on. */
91
+ export function seedRejectedInboundMessage(accountId: string, providerMessageId: string): void {
92
+ stateFor(accountId).log.push({ rejected: { providerMessageId, reason: "unparseable" } });
93
+ }
94
+
95
+ /** Test helper: the full version of a message that fetchContent serves later. */
96
+ export function setInboundFullContent(accountId: string, msg: RawInboundMessage): void {
97
+ stateFor(accountId).fullContent.set(msg.providerMessageId, msg);
98
+ }
99
+
100
+ /** Test helper: the provider message ids of every fetchContent call so far. */
101
+ export function contentFetchesOf(accountId: string): readonly (readonly string[])[] {
102
+ return stateFor(accountId).contentFetches;
103
+ }
104
+
73
105
  /** Test helper: injects a one-shot error into the next fetch (e.g.
74
106
  * InboundAuthError or InboundCursorInvalidError). */
75
107
  export function failNextFetchWith(accountId: string, err: unknown): void {
@@ -81,6 +113,25 @@ export function failNextVerifyWith(accountId: string, err: unknown): void {
81
113
  stateFor(accountId).nextVerifyError = err;
82
114
  }
83
115
 
116
+ /** Test helper: fetches of the account do not return until `release()`, whatever the abort signal
117
+ * says, like a provider that ignores cancellation on a dead connection. */
118
+ export function hangFetchesOf(accountId: string): { readonly release: () => void } {
119
+ const state = stateFor(accountId);
120
+ const { promise, resolve } = Promise.withResolvers<void>();
121
+ state.hangingFetch = promise;
122
+ return {
123
+ release: () => {
124
+ state.hangingFetch = null;
125
+ resolve();
126
+ },
127
+ };
128
+ }
129
+
130
+ /** Test helper: the abort signal the last fetch of the account received. */
131
+ export function lastFetchSignalOf(accountId: string): AbortSignal | undefined {
132
+ return stateFor(accountId).lastFetchSignal;
133
+ }
134
+
84
135
  /** Test helper: every fetch reads this tenant secret first, like a real
85
136
  * provider reads its stored credentials; a missing secret is an auth error. */
86
137
  export function requireCredentialSecretOnFetch(accountId: string, key: string): void {
@@ -116,6 +167,11 @@ export function isWatching(accountId: string): boolean {
116
167
  return accounts.get(accountId)?.watch != null;
117
168
  }
118
169
 
170
+ /** Test helper: the trustedAuthservId of the account the last watch was started with. */
171
+ export function watchedTrustedAuthservIdOf(accountId: string): string | null | undefined {
172
+ return accounts.get(accountId)?.watchedTrustedAuthservId;
173
+ }
174
+
119
175
  /** Test-Helper: kompletter State-Reset (Test-Isolation — Mock-States
120
176
  * nie zwischen Tests teilen). */
121
177
  export function resetInboundInMemory(): void {
@@ -142,6 +198,8 @@ export const inMemoryInboundMailPlugin: InboundMailProviderPlugin = {
142
198
  },
143
199
  fetch: async (ctx, account, cursor, opts): Promise<InboundFetchResult> => {
144
200
  const state = stateFor(account.id);
201
+ state.lastFetchSignal = opts.signal;
202
+ if (state.hangingFetch) await state.hangingFetch;
145
203
  if (state.nextFetchError) {
146
204
  const err = state.nextFetchError;
147
205
  state.nextFetchError = null;
@@ -153,15 +211,29 @@ export const inMemoryInboundMailPlugin: InboundMailProviderPlugin = {
153
211
  const offset = offsetOf(cursor);
154
212
  const slice = state.log.slice(offset, offset + opts.maxMessages);
155
213
  const nextOffset = offset + slice.length;
214
+ const rejected = slice.flatMap((entry) => ("rejected" in entry ? [entry.rejected] : []));
156
215
  return {
157
- messages: slice,
216
+ messages: slice.flatMap((entry) => ("message" in entry ? [entry.message] : [])),
217
+ ...(rejected.length > 0 && { rejected }),
158
218
  nextCursor: { offset: nextOffset },
159
219
  hasMore: nextOffset < state.log.length,
160
220
  };
161
221
  },
222
+ fetchContent: async (_ctx, account, providerMessageIds): Promise<InboundContentFetchResult> => {
223
+ const state = stateFor(account.id);
224
+ state.contentFetches.push([...providerMessageIds]);
225
+ const messages = providerMessageIds.flatMap((id) => state.fullContent.get(id) ?? []);
226
+ const found = new Set(messages.map((m) => m.providerMessageId));
227
+ return {
228
+ messages,
229
+ missing: providerMessageIds.filter((id) => !found.has(id)),
230
+ rejected: [],
231
+ };
232
+ },
162
233
  watch: async (_ctx, account, handlers) => {
163
234
  const state = stateFor(account.id);
164
235
  state.watch = handlers;
236
+ state.watchedTrustedAuthservId = account.trustedAuthservId ?? null;
165
237
  return async () => {
166
238
  if (state.watch === handlers) state.watch = null;
167
239
  };
@@ -1,13 +1,19 @@
1
1
  // Public API of the inbound-provider-inmemory bundled-feature.
2
2
 
3
3
  export {
4
+ contentFetchesOf,
4
5
  createInboundProviderInMemoryFeature,
5
6
  emitWatchError,
6
7
  failNextFetchWith,
7
8
  failNextVerifyWith,
9
+ hangFetchesOf,
8
10
  INMEMORY_PROVIDER_KEY,
9
11
  isWatching,
12
+ lastFetchSignalOf,
10
13
  requireCredentialSecretOnFetch,
11
14
  resetInboundInMemory,
12
15
  seedInboundMessage,
16
+ seedRejectedInboundMessage,
17
+ setInboundFullContent,
18
+ watchedTrustedAuthservIdOf,
13
19
  } from "./feature.ts";
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "version": "0.394.0",
4
+ "type": "fix",
5
+ "title": "catchUp job consumers ignore foreign event types before upcasting, and catchUp with maxPerTenant skip fails at boot",
6
+ "detail": "A catch-up job consumer now filters on the trigger's event types before the stored event is upcast, so a foreign event with a failing upcaster no longer stalls the job cursor. Boot rejects `trigger.catchUp` together with `maxPerTenant` onLimit \"skip\" (including the number shorthand): a replay must not discard history. Use onLimit \"defer\"."
7
+ },
8
+ {
9
+ "version": "0.394.0",
10
+ "type": "improvement",
11
+ "title": "r.job event triggers can catch up on stored events",
12
+ "detail": "trigger { on, catchUp: true } gives the job its own event consumer job:<jobQn> that starts at the beginning of the event store, so events from before the job existed run it too, once per event (job id derived from job QN and event id). Every \"on\" entry must be an r.defineEvent event, otherwise boot fails. The job is no longer fired by the shared job-trigger consumer. JobRunner gains handleCatchUpEvent."
13
+ },
14
+ {
15
+ "version": "0.394.0",
16
+ "type": "fix",
17
+ "title": "triggerName is documented as the firing event's qualified name",
18
+ "detail": "The JSDoc of `triggerName` said it names the handler that emitted the event; the job runner sets the event's qualified name, which multi-trigger jobs compare against the event QN constants."
19
+ },
2
20
  {
3
21
  "version": "0.390.0",
4
22
  "type": "fix",
@@ -16,6 +16,7 @@ export const MANAGED_PAGES_I18N: Readonly<Record<string, LocalizedString>> = {
16
16
  // Screen titles (global/unprefixed, screenTitleKey = screen:<id>.title)
17
17
  "screen:page-list.title": { en: "Pages" },
18
18
  "screen:page-edit.title": { en: "Edit page" },
19
+ "screen:page-edit.createTitle": { en: "New page" },
19
20
  "screen:branding-settings.title": { en: "Branding" },
20
21
 
21
22
  // Page-Entity Feld-Labels (managed-pages:entity:page:field:*)
@@ -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
+ }