@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
@@ -0,0 +1,172 @@
1
+ // Paddle webhook: signature check, then mapping of Paddle's event envelope onto
2
+ // the foundation's normalized events. Events without a valid tenant binding are
3
+ // dropped (200 ignored): see tenant-binding.ts.
4
+
5
+ import {
6
+ BillingEventKinds,
7
+ type PaymentEvent,
8
+ type SubscriptionEvent,
9
+ type SubscriptionEventType,
10
+ SubscriptionEventTypes,
11
+ } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
12
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
13
+ import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
14
+ import { parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
15
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
16
+ import * as z from "zod";
17
+ import { PADDLE_PROVIDER_NAME } from "./constants.ts";
18
+ import type { PaddleWebhookRuntime } from "./runtime.ts";
19
+ import {
20
+ mapPaddleSubscriptionState,
21
+ paddleCustomDataSchema,
22
+ paddleSubscriptionSchema,
23
+ } from "./subscription-state.ts";
24
+ import { boundTenantOf } from "./tenant-binding.ts";
25
+ import { verifyPaddleSignature } from "./verify-webhook.ts";
26
+
27
+ const PADDLE_SUBSCRIPTION_EVENT_TYPES: Readonly<Record<string, SubscriptionEventType>> = {
28
+ "subscription.created": SubscriptionEventTypes.created,
29
+ "subscription.activated": SubscriptionEventTypes.updated,
30
+ "subscription.updated": SubscriptionEventTypes.updated,
31
+ "subscription.trialing": SubscriptionEventTypes.updated,
32
+ "subscription.paused": SubscriptionEventTypes.updated,
33
+ "subscription.resumed": SubscriptionEventTypes.updated,
34
+ "subscription.past_due": SubscriptionEventTypes.invoicePaymentFailed,
35
+ "subscription.canceled": SubscriptionEventTypes.canceled,
36
+ };
37
+
38
+ const TRANSACTION_COMPLETED = "transaction.completed";
39
+
40
+ const envelopeSchema = z.object({
41
+ event_id: z.string().min(1),
42
+ event_type: z.string(),
43
+ occurred_at: z.iso.datetime({ offset: true }),
44
+ data: z.unknown(),
45
+ });
46
+
47
+ const paddleTransactionSchema = z.object({
48
+ id: z.string().min(1),
49
+ subscription_id: z.string().nullish(),
50
+ customer_id: z.string().min(1).nullish(),
51
+ custom_data: paddleCustomDataSchema,
52
+ items: z.array(
53
+ z.object({
54
+ price: z.object({ id: z.string().min(1) }),
55
+ quantity: z.number().int().positive().optional(),
56
+ }),
57
+ ),
58
+ });
59
+
60
+ export type PaddleWebhookOptions = {
61
+ readonly priceToTier: Readonly<Record<string, string>>;
62
+ /** Test seam: Unix seconds. Defaults to the wall clock. */
63
+ readonly nowSeconds?: () => number;
64
+ };
65
+
66
+ function currentUnixSeconds(): number {
67
+ return Math.floor(Temporal.Now.instant().epochMilliseconds / 1000);
68
+ }
69
+
70
+ function parsePaymentEvent(
71
+ providerEventId: string,
72
+ data: unknown,
73
+ bindingKey: string,
74
+ ): PaymentEvent | null {
75
+ const parsed = paddleTransactionSchema.safeParse(data);
76
+ if (!parsed.success) return null;
77
+ const transaction = parsed.data;
78
+ // A subscription's own payments are covered by its subscription.* events.
79
+ if (transaction.subscription_id) return null;
80
+ const priceId = transaction.items[0]?.price.id;
81
+ const quantity = transaction.items[0]?.quantity ?? 1;
82
+ const bound = boundTenantOf(transaction.custom_data, bindingKey);
83
+ if (!bound || !priceId || !transaction.customer_id) return null;
84
+ return {
85
+ kind: BillingEventKinds.payment,
86
+ providerEventId,
87
+ providerName: PADDLE_PROVIDER_NAME,
88
+ tenantId: bound.tenantId,
89
+ ...(bound.consentId !== undefined && { consentId: bound.consentId }),
90
+ providerCheckoutId: transaction.id,
91
+ providerCustomerId: transaction.customer_id,
92
+ priceId,
93
+ quantity,
94
+ };
95
+ }
96
+
97
+ // Subscription prices are meant to be quantity 1; the tier is still set, but a
98
+ // different quantity is worth an operator's attention. Event id and quantity only.
99
+ function warnOnUnexpectedQuantity(
100
+ providerEventId: string,
101
+ items: readonly { readonly quantity?: number | undefined }[],
102
+ ): void {
103
+ for (const item of items) {
104
+ if (item.quantity !== undefined && item.quantity !== 1) {
105
+ createFallbackLogger("subscription-paddle").warn(
106
+ "paddle subscription item quantity is not 1; the tier is still set",
107
+ { providerEventId, quantity: item.quantity },
108
+ );
109
+ }
110
+ }
111
+ }
112
+
113
+ function parseSubscriptionEvent(
114
+ providerEventId: string,
115
+ type: SubscriptionEventType,
116
+ providerEventAt: string,
117
+ data: unknown,
118
+ options: { readonly priceToTier: Readonly<Record<string, string>>; readonly bindingKey: string },
119
+ ): SubscriptionEvent | null {
120
+ const parsed = paddleSubscriptionSchema.safeParse(data);
121
+ if (!parsed.success) return null;
122
+ const sub = parsed.data;
123
+ const bound = boundTenantOf(sub.custom_data, options.bindingKey);
124
+ const state = mapPaddleSubscriptionState(sub, options.priceToTier);
125
+ if (!bound || !state) return null;
126
+ warnOnUnexpectedQuantity(providerEventId, sub.items);
127
+ return {
128
+ providerEventId,
129
+ providerName: PADDLE_PROVIDER_NAME,
130
+ type,
131
+ tenantId: bound.tenantId,
132
+ ...(bound.consentId !== undefined && { consentId: bound.consentId }),
133
+ providerCustomerId: sub.customer_id,
134
+ providerSubscriptionId: sub.id,
135
+ providerEventAt,
136
+ ...state,
137
+ };
138
+ }
139
+
140
+ export function verifyAndParsePaddleWebhook(
141
+ runtime: PaddleWebhookRuntime,
142
+ options: PaddleWebhookOptions,
143
+ ): (
144
+ rawBody: string,
145
+ headers: Record<string, string>,
146
+ systemSecrets?: SecretsContext,
147
+ ) => Promise<SubscriptionEvent | PaymentEvent | null> {
148
+ const nowSeconds = options.nowSeconds ?? currentUnixSeconds;
149
+ return async (rawBody, headers, systemSecrets) => {
150
+ const secret = await runtime.resolveWebhookSecret(systemSecrets);
151
+ verifyPaddleSignature(rawBody, headers, secret, nowSeconds());
152
+ // After verification only: an unauthenticated request must not trigger a second secret read.
153
+ const bindingKey = await runtime.resolveTenantBindingKey(systemSecrets);
154
+
155
+ // Past verification the body is trusted to be Paddle's; a malformed shape is
156
+ // schema drift, so it is dropped (200 ignored) instead of answered with a 401.
157
+ const envelope = envelopeSchema.safeParse(
158
+ parseJsonOrThrow<unknown>(rawBody, "subscription-paddle webhook body"),
159
+ );
160
+ if (!envelope.success) return null;
161
+
162
+ const { event_id: providerEventId, event_type: type, occurred_at, data } = envelope.data;
163
+ if (type === TRANSACTION_COMPLETED) return parsePaymentEvent(providerEventId, data, bindingKey);
164
+ if (!Object.hasOwn(PADDLE_SUBSCRIPTION_EVENT_TYPES, type)) return null;
165
+ const normalizedType = PADDLE_SUBSCRIPTION_EVENT_TYPES[type];
166
+ if (!normalizedType) return null;
167
+ return parseSubscriptionEvent(providerEventId, normalizedType, occurred_at, data, {
168
+ priceToTier: options.priceToTier,
169
+ bindingKey,
170
+ });
171
+ };
172
+ }
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.394.0",
4
+ "type": "fix",
5
+ "title": "collection trees and Save follow the server's access rules",
6
+ "detail": "textBlocksClient({ tenantId }) no longer sends tenantIdOverride for `ownership: \"user\"` collections, and a tenant-wide tree that the server answers with tenant_override_requires_system_admin reloads on the session tenant, so members see and edit their own entries instead of an error. The Save button follows the collection's own access rule (DEFAULT_COLLECTION_ACCESS when none is declared), the same rule the list, item and set handlers check; the public text-block pair keeps TenantAdmin or SystemAdmin. A save reloads the tree of its collection, so a new per-user entry shows without a page reload. The editor sets the page header title to the entry title, so the breadcrumb no longer shows the screen behind it."
7
+ },
8
+ {
9
+ "version": "0.394.0",
10
+ "type": "fix",
11
+ "title": "user-owned collection entries no longer fail with pii_ciphertext_leak",
12
+ "detail": "With a PII key service configured, the list and item queries of an `ownership: \"user\"` collection (signatures) returned the stored ciphertext, so the API rejected the response and the editor showed \"Could not load block\". Both queries now decrypt `content` for the owner before returning it."
13
+ },
2
14
  {
3
15
  "version": "0.390.0",
4
16
  "type": "fix",
@@ -8,7 +8,7 @@ import { DEFAULT_COLLECTION_ACCESS } from "../web/constants.ts";
8
8
  import {
9
9
  type CollectionEntryRow,
10
10
  collectionStore,
11
- toCollectionEntry,
11
+ readableCollectionEntry,
12
12
  } from "./collection-shared.ts";
13
13
 
14
14
  // Single entry of one collection, for the editor behind a tree node. Same
@@ -40,7 +40,7 @@ export function makeCollectionItemQuery(collection: ContentCollectionDefinition)
40
40
  });
41
41
 
42
42
  if (!row) return null;
43
- return toCollectionEntry(row);
43
+ return readableCollectionEntry(row, store, `${collection.id}-item`);
44
44
  },
45
45
  });
46
46
  }
@@ -8,7 +8,7 @@ import { DEFAULT_COLLECTION_ACCESS } from "../web/constants.ts";
8
8
  import {
9
9
  type CollectionEntryRow,
10
10
  collectionStore,
11
- toCollectionEntry,
11
+ readableCollectionEntry,
12
12
  } from "./collection-shared.ts";
13
13
 
14
14
  // One list handler per declared collection: `kind`, `ownership` and `access`
@@ -42,7 +42,11 @@ export function makeCollectionListQuery(collection: ContentCollectionDefinition)
42
42
  ...store.scopeOf(query.user),
43
43
  }),
44
44
  );
45
- return { blocks: rows.map(toCollectionEntry) };
45
+ return {
46
+ blocks: await Promise.all(
47
+ rows.map((row) => readableCollectionEntry(row, store, `${collection.id}-list`)),
48
+ ),
49
+ };
46
50
  },
47
51
  });
48
52
  }
@@ -1,3 +1,8 @@
1
+ import { requestContext } from "@cosmicdrift/kumiko-framework/api";
2
+ import {
3
+ configuredPiiSubjectKms,
4
+ decryptPiiFieldValues,
5
+ } from "@cosmicdrift/kumiko-framework/crypto";
1
6
  import type { EventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
7
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
3
8
  import type {
@@ -41,6 +46,8 @@ export type CollectionStore = {
41
46
  scopeOf(user: SessionUser): Readonly<Record<string, unknown>>;
42
47
  /** Columns a create needs beyond the editable ones the payload carries. */
43
48
  createDefaults(tenantId: TenantId): Readonly<Record<string, unknown>>;
49
+ /** True when `content` is stored as PII ciphertext and must be decrypted on read. */
50
+ readonly contentIsPii: boolean;
44
51
  };
45
52
 
46
53
  // `ownership: "user"` means every user keeps their own entries (signatures);
@@ -55,6 +62,7 @@ export function collectionStore(collection: ContentCollectionDefinition): Collec
55
62
  executor: userContentExecutor,
56
63
  scopeOf: (user) => ({ ownerId: user.id }),
57
64
  createDefaults: () => ({}),
65
+ contentIsPii: true,
58
66
  };
59
67
  }
60
68
  return {
@@ -68,9 +76,26 @@ export function collectionStore(collection: ContentCollectionDefinition): Collec
68
76
  parentTemplateId: null,
69
77
  status: "active",
70
78
  }),
79
+ contentIsPii: false,
71
80
  };
72
81
  }
73
82
 
83
+ // Raw reads return the stored column, which is ciphertext for a user-owned entry while a KMS
84
+ // is configured; the API's leak guard rejects any response that still carries it.
85
+ export async function readableCollectionEntry(
86
+ row: CollectionEntryRow,
87
+ store: CollectionStore,
88
+ fallbackRequestId: string,
89
+ ) {
90
+ const kms = configuredPiiSubjectKms();
91
+ if (!store.contentIsPii || !kms || row.content === null) return toCollectionEntry(row);
92
+ const decrypted = await decryptPiiFieldValues({ content: row.content }, ["content"], kms, {
93
+ requestId: requestContext.get()?.requestId ?? fallbackRequestId,
94
+ });
95
+ const content = decrypted["content"];
96
+ return toCollectionEntry({ ...row, content: typeof content === "string" ? content : null });
97
+ }
98
+
74
99
  export function toCollectionEntry(row: CollectionEntryRow) {
75
100
  return {
76
101
  slug: row.slug,