@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
@@ -1,126 +0,0 @@
1
- // Call-time key/flag resolution for the subscription-dodo plugin, mirroring
2
- // subscription-stripe/runtime.ts: credentials come from backing:"secrets"
3
- // config keys under SYSTEM_TENANT_ID, so rotation and go-live need no redeploy.
4
- // The factory options survive as env-bridge fallbacks.
5
- //
6
- // Two read paths: post-tenant handlers read the key audited through the
7
- // HandlerContext; the pre-tenant webhook reads the secret un-audited from the
8
- // system SecretsContext the foundation passes in.
9
-
10
- import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
11
- import {
12
- type ConfigKeyHandle,
13
- type HandlerContext,
14
- SYSTEM_TENANT_ID,
15
- } from "@cosmicdrift/kumiko-framework/engine";
16
- import { FeatureDisabledError, UnconfiguredError } from "@cosmicdrift/kumiko-framework/errors";
17
- import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
18
- import { parseJsonSafe } from "@cosmicdrift/kumiko-framework/utils";
19
- import { createDodoHttp, type DodoRequest, type DodoSend } from "./client.ts";
20
- import { DODO_BASE_URLS, DodoEnvironments, SUBSCRIPTION_DODO_FEATURE } from "./constants.ts";
21
-
22
- const API_KEY_HINT =
23
- "Set the Dodo Payments API key via config:write:set on `subscription-dodo:config:api-key` (or seed it from DODO_PAYMENTS_API_KEY during the env bridge).";
24
-
25
- export type DodoRuntimeDeps = {
26
- readonly apiKeyHandle: ConfigKeyHandle<"text">;
27
- readonly webhookSecretHandle: ConfigKeyHandle<"text">;
28
- readonly billingLiveHandle: ConfigKeyHandle<"boolean">;
29
- readonly environmentHandle: ConfigKeyHandle<"select">;
30
- readonly send: DodoSend;
31
- readonly fallback: { readonly apiKey?: string; readonly webhookSecret?: string };
32
- };
33
-
34
- // backing:"secrets" values are stored JSON-serialized, so the store read has to
35
- // be parsed back; the env fallback is a plain string and stays raw. The error
36
- // must not carry the input: the JSON parser's own message would repeat the
37
- // credential into logs and, for webhooks, into the 401 body.
38
- function parseStoredSecret(raw: string | undefined): string | undefined {
39
- if (raw === undefined || raw.length === 0) return undefined;
40
- const parsed = parseJsonSafe<unknown>(raw, undefined);
41
- if (parsed === undefined) {
42
- throw new Error("subscription-dodo credential (backing:secrets) is not valid JSON");
43
- }
44
- return typeof parsed === "string" && parsed.length > 0 ? parsed : undefined;
45
- }
46
-
47
- export type DodoCtxRuntime = {
48
- readonly call: (ctx: HandlerContext, request: DodoRequest) => Promise<unknown>;
49
- readonly assertBillingLive: (ctx: HandlerContext, handlerName?: string) => Promise<void>;
50
- readonly isBillingEnabled: (ctx: HandlerContext) => Promise<boolean>;
51
- };
52
-
53
- export type DodoWebhookRuntime = {
54
- readonly resolveWebhookSecret: (systemSecrets?: SecretsContext) => Promise<string>;
55
- };
56
-
57
- export type DodoRuntimes = {
58
- readonly ctx: DodoCtxRuntime;
59
- readonly webhook: DodoWebhookRuntime;
60
- };
61
-
62
- export function createDodoRuntimes(deps: DodoRuntimeDeps): DodoRuntimes {
63
- const http = createDodoHttp(deps.send);
64
-
65
- async function ctxApiKey(ctx: HandlerContext): Promise<string> {
66
- let key: string | undefined;
67
- if (ctx.services.secrets) {
68
- const got = await requireSecretsContext(
69
- { secrets: ctx.services.secrets, user: ctx.user },
70
- SUBSCRIPTION_DODO_FEATURE,
71
- ).get(SYSTEM_TENANT_ID, deps.apiKeyHandle.name);
72
- key = parseStoredSecret(got?.reveal());
73
- }
74
- if (!key && deps.fallback.apiKey && deps.fallback.apiKey.length > 0) {
75
- key = deps.fallback.apiKey;
76
- }
77
- if (!key) {
78
- throw new UnconfiguredError({
79
- feature: SUBSCRIPTION_DODO_FEATURE,
80
- key: deps.apiKeyHandle.name,
81
- hint: API_KEY_HINT,
82
- });
83
- }
84
- return key;
85
- }
86
-
87
- async function baseUrlFor(ctx: HandlerContext): Promise<string> {
88
- const environment = ctx.config ? await ctx.config(deps.environmentHandle) : undefined;
89
- return DODO_BASE_URLS[environment === DodoEnvironments.live ? "live" : "test"];
90
- }
91
-
92
- return {
93
- ctx: {
94
- call: async (ctx, request) =>
95
- http({ baseUrl: await baseUrlFor(ctx), apiKey: await ctxApiKey(ctx) }, request),
96
- assertBillingLive: async (ctx, handlerName = "create-checkout-session") => {
97
- const live = ctx.config ? await ctx.config(deps.billingLiveHandle) : undefined;
98
- if (live !== true) throw new FeatureDisabledError(SUBSCRIPTION_DODO_FEATURE, handlerName);
99
- },
100
- isBillingEnabled: async (ctx) => {
101
- const live = ctx.config ? await ctx.config(deps.billingLiveHandle) : undefined;
102
- if (live !== true) return false;
103
- if (deps.fallback.apiKey !== undefined && deps.fallback.apiKey.length > 0) return true;
104
- // Existence-only probe on every billing-plans render: must stay un-audited.
105
- return ctx.services.secrets
106
- ? await ctx.services.secrets.has(SYSTEM_TENANT_ID, deps.apiKeyHandle.name)
107
- : false;
108
- },
109
- },
110
- webhook: {
111
- resolveWebhookSecret: async (systemSecrets) => {
112
- if (systemSecrets) {
113
- // No auditCtx: every webhook would otherwise spam the audit trail.
114
- const got = await systemSecrets.get(SYSTEM_TENANT_ID, deps.webhookSecretHandle.name);
115
- const stored = parseStoredSecret(got?.reveal());
116
- if (stored) return stored;
117
- }
118
- const fallback = deps.fallback.webhookSecret;
119
- if (fallback && fallback.length > 0) return fallback;
120
- throw new Error(
121
- `subscription-dodo: '${deps.webhookSecretHandle.name}' unresolved — no system-secret set and no factory fallback. Webhook cannot verify until it is configured.`,
122
- );
123
- },
124
- },
125
- };
126
- }
@@ -1,262 +0,0 @@
1
- // Dodo Payments webhook: Standard Webhooks signature verification plus mapping
2
- // of Dodo's event envelope onto the foundation's normalized events.
3
- //
4
- // Verification always runs against the raw body string; re-serialized JSON
5
- // would change whitespace and break the HMAC. The signature check happens
6
- // before any parsing, so no path bypasses it.
7
-
8
- import { createHmac, timingSafeEqual } from "node:crypto";
9
- import {
10
- BillingEventKinds,
11
- type PaymentEvent,
12
- parseProviderConsentId,
13
- type SubscriptionEvent,
14
- type SubscriptionEventType,
15
- SubscriptionEventTypes,
16
- type SubscriptionStatus,
17
- SubscriptionStatuses,
18
- } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
19
- import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
20
- import { parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
21
- import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
22
- import * as z from "zod";
23
- import { DODO_PROVIDER_NAME } from "./constants.ts";
24
- import type { DodoWebhookRuntime } from "./runtime.ts";
25
-
26
- export const WEBHOOK_TOLERANCE_SECONDS = 300;
27
-
28
- const SECRET_PREFIX = "whsec_";
29
- const SIGNATURE_VERSION = "v1";
30
- const TIMESTAMP_PATTERN = /^\d+$/;
31
-
32
- export type StandardWebhookHeaders = Readonly<Record<string, string>>;
33
-
34
- function signatureFor(secretKey: Buffer, signedContent: string): Buffer {
35
- return createHmac("sha256", secretKey).update(signedContent).digest();
36
- }
37
-
38
- function matchesAnyV1Signature(header: string, expected: Buffer): boolean {
39
- return header.split(" ").some((entry) => {
40
- const [version, encoded] = entry.split(",");
41
- if (version !== SIGNATURE_VERSION || !encoded) return false;
42
- const candidate = Buffer.from(encoded, "base64");
43
- return candidate.length === expected.length && timingSafeEqual(candidate, expected);
44
- });
45
- }
46
-
47
- /** Throws unless the Standard Webhooks signature over `rawBody` is valid and
48
- * the timestamp is within the tolerance in both directions. */
49
- export function verifyStandardWebhook(
50
- rawBody: string,
51
- headers: StandardWebhookHeaders,
52
- secret: string,
53
- nowSeconds: number,
54
- ): void {
55
- const id = headers["webhook-id"];
56
- const timestamp = headers["webhook-timestamp"];
57
- const signatureHeader = headers["webhook-signature"];
58
- if (!id || !timestamp || !signatureHeader) {
59
- throw new Error(
60
- "subscription-dodo: webhook-id, webhook-timestamp or webhook-signature missing",
61
- );
62
- }
63
- if (!TIMESTAMP_PATTERN.test(timestamp)) {
64
- throw new Error("subscription-dodo: webhook-timestamp is not a Unix timestamp in seconds");
65
- }
66
- if (Math.abs(nowSeconds - Number(timestamp)) > WEBHOOK_TOLERANCE_SECONDS) {
67
- throw new Error("subscription-dodo: webhook timestamp outside the tolerance window");
68
- }
69
- const secretKey = Buffer.from(
70
- secret.startsWith(SECRET_PREFIX) ? secret.slice(SECRET_PREFIX.length) : secret,
71
- "base64",
72
- );
73
- const expected = signatureFor(secretKey, `${id}.${timestamp}.${rawBody}`);
74
- if (!matchesAnyV1Signature(signatureHeader, expected)) {
75
- throw new Error("subscription-dodo: webhook signature verify failed");
76
- }
77
- }
78
-
79
- // =============================================================================
80
- // Event mapping
81
- // =============================================================================
82
-
83
- const DODO_SUBSCRIPTION_EVENT_TYPES: Readonly<Record<string, SubscriptionEventType>> = {
84
- "subscription.active": SubscriptionEventTypes.created,
85
- "subscription.updated": SubscriptionEventTypes.updated,
86
- "subscription.plan_changed": SubscriptionEventTypes.updated,
87
- "subscription.paused": SubscriptionEventTypes.updated,
88
- "subscription.unpaused": SubscriptionEventTypes.updated,
89
- "subscription.update_payment_method": SubscriptionEventTypes.updated,
90
- "subscription.renewed": SubscriptionEventTypes.invoicePaid,
91
- "subscription.past_due": SubscriptionEventTypes.invoicePaymentFailed,
92
- "subscription.on_hold": SubscriptionEventTypes.invoicePaymentFailed,
93
- "subscription.cancelled": SubscriptionEventTypes.canceled,
94
- "subscription.expired": SubscriptionEventTypes.canceled,
95
- "subscription.failed": SubscriptionEventTypes.canceled,
96
- };
97
-
98
- const DODO_SUBSCRIPTION_STATUSES: Readonly<Record<string, SubscriptionStatus>> = {
99
- pending: SubscriptionStatuses.incomplete,
100
- active: SubscriptionStatuses.active,
101
- on_hold: SubscriptionStatuses.pastDue,
102
- past_due: SubscriptionStatuses.pastDue,
103
- paused: SubscriptionStatuses.pastDue,
104
- cancelled: SubscriptionStatuses.canceled,
105
- failed: SubscriptionStatuses.canceled,
106
- expired: SubscriptionStatuses.canceled,
107
- };
108
-
109
- const PAYMENT_SUCCEEDED = "payment.succeeded";
110
-
111
- const metadataSchema = z.record(z.string(), z.unknown()).nullish();
112
- const customerSchema = z.object({ customer_id: z.string().min(1) });
113
-
114
- const envelopeSchema = z.object({
115
- type: z.string(),
116
- timestamp: z.iso.datetime({ offset: true }),
117
- data: z.unknown(),
118
- });
119
-
120
- export const dodoSubscriptionSchema = z.object({
121
- subscription_id: z.string().min(1),
122
- product_id: z.string().min(1),
123
- status: z.string(),
124
- customer: customerSchema,
125
- metadata: metadataSchema,
126
- next_billing_date: z.iso.datetime({ offset: true }),
127
- cancel_at_next_billing_date: z.boolean().nullish(),
128
- });
129
- export type DodoSubscription = z.infer<typeof dodoSubscriptionSchema>;
130
-
131
- const dodoPaymentSchema = z.object({
132
- subscription_id: z.string().nullish(),
133
- checkout_session_id: z.string().nullish(),
134
- customer: customerSchema,
135
- metadata: metadataSchema,
136
- product_cart: z.array(z.object({ product_id: z.string().min(1) })).nullish(),
137
- });
138
-
139
- function metadataString(metadata: DodoSubscription["metadata"], key: string): string | undefined {
140
- const value = metadata?.[key];
141
- return typeof value === "string" && value.length > 0 ? value : undefined;
142
- }
143
-
144
- export type MappedDodoSubscriptionState = {
145
- readonly status: SubscriptionStatus;
146
- readonly tier: string;
147
- readonly currentPeriodEnd: string;
148
- readonly cancelAt: string | null;
149
- };
150
-
151
- /** Shared by the webhook and retrieveSubscription so both map Dodo state
152
- * identically. Null when the status is unknown or the product is not in
153
- * `priceToTier` (SubscriptionEvent.tier is required). */
154
- export function mapDodoSubscriptionState(
155
- sub: DodoSubscription,
156
- priceToTier: Readonly<Record<string, string>>,
157
- ): MappedDodoSubscriptionState | null {
158
- const status = DODO_SUBSCRIPTION_STATUSES[sub.status];
159
- const tier = priceToTier[sub.product_id];
160
- if (!status || !tier) return null;
161
- return {
162
- status,
163
- tier,
164
- currentPeriodEnd: sub.next_billing_date,
165
- cancelAt: sub.cancel_at_next_billing_date === true ? sub.next_billing_date : null,
166
- };
167
- }
168
-
169
- export type DodoWebhookOptions = {
170
- readonly priceToTier: Readonly<Record<string, string>>;
171
- /** Test seam: Unix seconds. Defaults to the wall clock. */
172
- readonly nowSeconds?: () => number;
173
- };
174
-
175
- function currentUnixSeconds(): number {
176
- return Math.floor(Temporal.Now.instant().epochMilliseconds / 1000);
177
- }
178
-
179
- function parsePaymentEvent(providerEventId: string, data: unknown): PaymentEvent | null {
180
- const parsed = dodoPaymentSchema.safeParse(data);
181
- if (!parsed.success) return null;
182
- const payment = parsed.data;
183
- // A subscription's own payments are covered by its subscription.* events.
184
- if (payment.subscription_id) return null;
185
- const tenantId = metadataString(payment.metadata, "tenantId");
186
- const priceId = payment.product_cart?.[0]?.product_id;
187
- if (!tenantId || !priceId) return null;
188
- const consentId = parseProviderConsentId(payment.metadata?.["consentId"]);
189
- return {
190
- kind: BillingEventKinds.payment,
191
- providerEventId,
192
- providerName: DODO_PROVIDER_NAME,
193
- tenantId,
194
- ...(consentId !== undefined && { consentId }),
195
- ...(payment.checkout_session_id && { providerCheckoutId: payment.checkout_session_id }),
196
- providerCustomerId: payment.customer.customer_id,
197
- priceId,
198
- };
199
- }
200
-
201
- function parseSubscriptionEvent(
202
- providerEventId: string,
203
- type: SubscriptionEventType,
204
- providerEventAt: string,
205
- data: unknown,
206
- priceToTier: Readonly<Record<string, string>>,
207
- ): SubscriptionEvent | null {
208
- const parsed = dodoSubscriptionSchema.safeParse(data);
209
- if (!parsed.success) return null;
210
- const sub = parsed.data;
211
- const tenantId = metadataString(sub.metadata, "tenantId");
212
- const state = mapDodoSubscriptionState(sub, priceToTier);
213
- if (!tenantId || !state) return null;
214
- const consentId = parseProviderConsentId(sub.metadata?.["consentId"]);
215
- return {
216
- providerEventId,
217
- providerName: DODO_PROVIDER_NAME,
218
- type,
219
- tenantId,
220
- ...(consentId !== undefined && { consentId }),
221
- providerCustomerId: sub.customer.customer_id,
222
- providerSubscriptionId: sub.subscription_id,
223
- providerEventAt,
224
- ...state,
225
- };
226
- }
227
-
228
- export function verifyAndParseDodoWebhook(
229
- runtime: DodoWebhookRuntime,
230
- options: DodoWebhookOptions,
231
- ): (
232
- rawBody: string,
233
- headers: Record<string, string>,
234
- systemSecrets?: SecretsContext,
235
- ) => Promise<SubscriptionEvent | PaymentEvent | null> {
236
- const nowSeconds = options.nowSeconds ?? currentUnixSeconds;
237
- return async (rawBody, headers, systemSecrets) => {
238
- const secret = await runtime.resolveWebhookSecret(systemSecrets);
239
- verifyStandardWebhook(rawBody, headers, secret, nowSeconds());
240
-
241
- // Past verification the body is trusted to be Dodo's; a malformed shape is
242
- // schema drift, so it is dropped (200 ignored) instead of answered with a 401.
243
- const envelope = envelopeSchema.safeParse(
244
- parseJsonOrThrow<unknown>(rawBody, "subscription-dodo webhook body"),
245
- );
246
- // verifyStandardWebhook has checked webhook-id is present.
247
- const providerEventId = headers["webhook-id"];
248
- if (!envelope.success || !providerEventId) return null;
249
-
250
- const { type, timestamp, data } = envelope.data;
251
- if (type === PAYMENT_SUCCEEDED) return parsePaymentEvent(providerEventId, data);
252
- const normalizedType = DODO_SUBSCRIPTION_EVENT_TYPES[type];
253
- if (!normalizedType) return null;
254
- return parseSubscriptionEvent(
255
- providerEventId,
256
- normalizedType,
257
- timestamp,
258
- data,
259
- options.priceToTier,
260
- );
261
- };
262
- }